news 2026/9/8 12:29:48

从stdio到HTTP:MCP Server远程调用改造实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从stdio到HTTP:MCP Server远程调用改造实战指南

如果你手里有一个能用的 MCP server,大概率它跑在 stdio 上——启动一个本地进程,用标准输入输出跟 Claude Desktop、Cursor 这类客户端“聊天”。这套机制在单机场景非常顺,可一旦你想让另一台机器上的客户端也调它,stdio 就彻底没招了。我最近就把好几个本地 stdio MCP 服务转成了 HTTP MCP,放在服务器上让团队同事远程调用,整个过程试过现成工具、也自己写过包装服务。今天这篇就把转换方案、工具选型和踩过的坑完整捋一遍,给同样被 stdio 卡住的人一个可以直接抄的作业。

先说清楚适配人群:你至少跑通过一个 MCP server,知道 tools、prompts、resources 大概是什么;你手头有明显跑在 stdio 下的服务,比如文件系统、数据库、浏览器自动化这类;你想让局域网或云服务器另一端的客户端也能连上来,又不想为每个客户端各维护一份本地进程。下面所有内容都围绕“如何把 stdio transport 改造成 HTTP transport”展开,核心是让你少走我走过的弯路。

1. MCP 的传输方式:为什么 stdio 只适合单机

1.1 MCP 到底是“什么”

MCP(Model Context Protocol)说白了就是给 AI 客户端和外部工具之间定的一套统一接口规范。过去每个 AI 应用接外部数据源都是各写各的插件,换个客户端就要重写一遍对接逻辑。MCP 把“工具长什么样”“客户端怎么调用”“结果怎么返回”全部标准化,于是 Claude、Cursor、各种 IDE 可以共用同一套 MCP server。

很多人会把 MCP 和 Computer Use 混在一起。其实两者完全不是一个层面的东西:Computer Use 是模型主动操作屏幕、键盘、鼠标的能力,本质是“模型会动手”;MCP 是模型和外部能力之间的一张 API 契约,本质是“能力能被发现和调用”。两者可以配合用,但别当成一回事。

MCP 协议在设计时就考虑了传输层的可替换性。官方规范里定义了两种主流传输方式——stdio 和 HTTP(早期是 HTTP+SSE,后来演进成 Streamable HTTP)。传输方式不同,决定了你的 MCP server 能被谁访问、怎么访问。

1.2 三种传输方式的真实区别

先看 stdio。这种模式下,客户端负责拉起一个子进程,MCP server 以子进程的方式运行,客户端往子进程的标准输入写 JSON-RPC 消息,从标准输出读结果。好处很明显:进程隔离、权限独立、不需要监听任何端口、没有网络暴露面,非常适合个人电脑上的本地工具。

缺点同样明显:server 的生命周期必须由某个客户端进程直接管理,远程客户端根本没法拉起你机器上的子进程;同一时刻基本只能服务一个“父进程”;而且进程的启动、重启、崩溃恢复全靠客户端心情。你让 Claude Desktop 拉起一个本地文件服务很舒服,但你没法让同一台机器上的另一个客户端去复用它,更别提让隔壁工位的同事来调。

再看 HTTP 系。早期大家用的是“HTTP+SSE”模式,也就是服务端通过 SSE(Server-Sent Events)向客户端单向推流,客户端再通过独立的 POST 端点回传消息,实际上要维护两条通道、两个 URL,浏览器和代理层处理起来都麻烦。后来官方推出了 Streamable HTTP,把所有交互收敛到一个 HTTP 端点上,客户端可以 POST 请求,服务端既能直接返回完整 JSON,也能返回 SSE 流,复杂度比旧方案低了一大截。

三者的取舍其实很清晰,我用一个表总结:

传输方式连接模型是否支持远程生命周期管理典型用途
stdio子进程 stdin/stdout不支持由客户端直接拉起本地个人工具、开发调试
HTTP+SSE(旧)SSE 推流 + POST 回传支持独立服务进程兼容老客户端的远程部署
Streamable HTTP(新)单端点 POST/SSE支持独立服务进程生产环境远程 MCP server

1.3 什么场景逼着你必须转 HTTP

我在实际中遇到的无非这三类需求。

第一类是多客户端共享。数据库 MCP、蓝湖 MCP 这类服务,如果团队五个人各在本地跑一份,配置稍微不一致就会出现“你这能查数据我这就报错”的尴尬。把服务统一部署到测试服务器上,大家走同一个地址,配置只有一处,问题也只在那一处排查。

第二类是客户端环境不支持拉子进程。有些远程开发容器、云 IDE 或者轻量客户端,只认 URL 形式的 MCP 配置,没有本地 Node/Python 运行时可以拉起 stdio 进程。这时候必须有一个外部地址给它。

第三类是工具和客户端不在同一台机器。你的开发机是 Windows,但 MCP server 依赖的模型服务跑在 Linux 服务器上;或者你人不在办公室,需要从家里连回内网的服务。stdio 天然跨不了机器,只有 HTTP 能跨。

需要说明的是,转成 HTTP 之后,MCP 本身的协议语义没有任何变化——仍然是 JSON-RPC、仍然是 initialize、tools/list、tools/call 那套流程。变的只是传输外壳。理解这一点,后面所有配置都是顺理成章的。

2. 工具选型:现成转换器 vs 自己写 HTTP server

2.1 先说结论:多数人直接用 supergateway

把 stdio MCP 包装成 HTTP MCP,社区里最成熟的开源方案就是 supergateway。它做的事情非常纯粹:你给它一个“启动 stdio server 的命令”,它在指定端口启动一个 HTTP 服务,对外提供 SSE 端点和 message 端点,内部帮你完成 stdio 与 HTTP 之间的双向桥接。现有 MCP server 的代码一行不用改。

supergateway 还顺带解决了几个实际部署问题:它支持通过参数设置访问令牌,客户端必须带 Bearer Token 才能调工具;支持 CORS 配置,方便网页端调用;支持多路由功能,可以在一个进程里按不同路径转发到不同的 stdio server。对中小团队来说,一个命令就能把“本地私有工具”变成“团队共享服务”,性价比极高。

它的限制也很明显:毕竟只是桥接层,你没法在转发过程中对工具做细粒度鉴权或二次加工,比如“某人只能用其中两个工具、其他人全部可用”这种需求它管不了。遇到这种诉求,就得往下走,自己写一个原生 HTTP transport 的 MCP server。

2.2 自己写原生 HTTP server 的适用场景

如果你打算从头开发一个新 MCP 服务,或者想把现有 server 做深度的权限控制、请求审计、流量统计,那就不该再绕 stdio 这层。直接用官方 TypeScript SDK 或 Python SDK(比如 FastMCP)写一个原生 HTTP transport 的服务,天然就支持远程调用,还能把鉴权逻辑集成进 Web 框架里。

代价是需要写代码、处理部署。好处是彻底摆脱进程管理,可以像部署普通 Web 服务一样用 systemd、Docker、负载均衡来编排。

2.3 为什么选了现成转换而不是重写

我自己实际跑过的项目里,绝大多数现有 MCP server 都是第三方维护的,比如文件系统服务、Playwright 浏览器自动化、数据库查询服务。让我去改这些源码不现实,fork 一份又得长期跟上游合并,维护成本太高。所以对“已有 stdio server 想开放远程调用”这个诉求,supergateway 就是最优解,改造风险几乎为零。

如果你只是临时需要把远程 HTTP MCP 暴露回本地 stdio 给某些老客户端用,方向是反过来的,可以关注官方提供的mcp-remote这类工具,它把远程 URL“伪装”成本地 stdio 进程。但这属于客户端侧适配,和我们今天讲的服务端发布不是一回事,别混淆。

3. 完整实操:一条命令把 stdio 变成 HTTP

3.1 第一步:准备一个可用的 stdio MCP server

为了演示,我用官方维护的 filesystem server,它可以安全地读写你指定的目录。先在本地确认它能正常运行:

npx -y @modelcontextprotocol/server-filesystem /tmp/mcp-data

如果终端没有报错、进程能一直挂着,说明 stdio server 本身没问题,可以进入下一步。这一步千万别省——很多转换失败根本不是 HTTP 层的问题,而是 stdio server 在非交互环境下压根起不来。

顺便说一句,我在这里特意用 Node 环境跑 npx,实际生产环境建议先把这个启动命令封装成脚本或直接安装到全局,避免每次桥接服务重启时还要去下载依赖。

3.2 第二步:启动 supergateway 做桥接

用 npx 直接启动:

npx -y supergateway \ --stdio "npx -y @modelcontextprotocol/server-filesystem /tmp/mcp-data" \ --port 8000 \ --apiKey "change-this-token" \ --cors "*"

参数说明:

  • --stdio:你要桥接的 stdio server 启动命令。注意用双引号包住整个命令,supergateway 会通过 shell 执行它。
  • --port:HTTP 服务监听端口。
  • --apiKey:访问令牌,客户端调用时必须携带。
  • --cors:允许的跨域来源,开发阶段可以放开,生产环境建议收紧成具体域名。

如果一切正常,日志里会显示 server 已启动,并列出两个关键端点:SSE 端点和 message 端点。可以先用 curl 快速验证一下 SSH 端点能不能连:

curl -N http://127.0.0.1:8000/sse

正常情况下终端会停留在一个已建立的连接上,不会立刻返回。看到这个样子就说明桥接层已经起来了。message 端点的验证需要带认证头,我们放到客户端配置那一步统一测。

3.3 第三步:在客户端里配置远程 MCP

以 Cursor 为例(Claude Desktop 新版也支持类似配置),在项目根目录或全局的 MCP 配置里加一条:

{ "mcpServers": { "remote-fs": { "type": "http", "url": "http://127.0.0.1:8000/sse", "headers": { "Authorization": "Bearer change-this-token" } } } }

关键就三点:type必须是 http 而不是 stdio;url填上一步验证过的 SSE 端点;headers里带上 Authorization 头,token 和启动 supergateway 时传的--apiKey保持一致。

配好之后回到聊天界面,问一句“帮我看看 /tmp/mcp-data 目录下有哪些文件”。如果配置正确,客户端会先列出这个远程 MCP server 提供的工具,然后调用、返回结果。看到这一步,整条链路就通了。

3.4 多个 stdio server 怎么办

实际部署时很少只转一个服务。最简单的做法是每个 server 开一个端口,比如文件服务占 8000,数据库服务占 8001,客户端配置多个远程 MCP 即可。这种做法最直观,排查问题也容易定位到端口。

如果端口资源紧张或者想统一入口,supergateway 也支持在单进程内配置多路由,把不同路径映射到不同 stdio 命令。不过多路由配置对参数的拼写比较敏感,我建议先跑通单端口方案,再在预发布环境里试路由高级用法,不要一上来就挑战高难度。

3.5 用 systemd 管理桥接服务

直接用 npx 启动的服务,终端一关就没了。生产环境建议写成 systemd 服务:

[Unit] Description=Supergateway MCP Bridge After=network.target [Service] User=mcp WorkingDirectory=/opt/mcp ExecStart=/usr/bin/npx -y supergateway --stdio "node /opt/mcp/fs-server.js /tmp/mcp-data" --port 8000 --apiKey "change-this-token" Restart=always RestartSec=3 Environment=PATH=/usr/bin:/usr/local/bin [Install] WantedBy=multi-user.target

这里有个很容易踩的坑:systemd 环境下的 PATH 和交互式 shell 不一样,经常出现npx: command not found或找不到 node 的问题。所以 ExecStart 里最好写绝对路径,或者在 Environment 里显式补上 PATH。服务跑起来后用systemctl statusjournalctl -u mcp-bridge -f看日志,supergateway 和 stdio 子进程的输出都会打进去。

3.6 验证链路的通用方法

不管用哪个客户端,调试时最有效的方法永远是绕过客户端直接用 curl 测。MCP 握手流程是固定的:先建立 SSE 连接,然后客户端通过 message 端点发 JSON-RPC 消息。你可以先发一个 initialize:

curl -X POST http://127.0.0.1:8000/message \ -H "Authorization: Bearer change-this-token" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"0.1"}}}'

能拿到 JSON 响应,说明 HTTP 通道和认证都通了。再发 tools/list 看看工具清单是否完整。这个习惯能帮你快速区分“是客户端配置问题”还是“server 桥接问题”。

4. 远程安全调用:三层防线一个都不能少

4.1 为什么裸奔的 MCP 非常危险

把 MCP server 开放到网络,等于把一堆高权限工具暴露给所有能访问这个地址的人。文件系统工具可以读写目录、数据库工具可以执行 SQL、浏览器自动化工具能操作网页。这些能力落到 AI 手里很强大,落到恶意请求手里就是灾难。

我见过最离谱的案例是有人把数据库 MCP 直接挂在公网,端口上连 token 都没设,任何人都能往 POST 接口发 tools/call 执行任意 SQL。这就不是“给陌生人发钥匙”了,是“直接把保险柜密码写在门上”。所以下面的三层防线,每一层都别省。

4.2 第一层:认证上锁

supergateway 的--apiKey就是最基础的认证闸门。启用后,所有对 message 端点的请求都必须携带Authorization: Bearer <token>,否则直接拒绝。Token 一定不能复用默认值,建议用openssl rand -hex 32生成随机串,长度 64 位起步。

如果你的场景是多个客户端连接同一个服务,建议每个客户端用独立 token,这样某个人把 token 泄露了,你只需要吊销那一个,而不是推倒重来。

4.3 第二层:TLS 加密传输

HTTP 明文传输意味着 token 和业务数据在网络里裸奔。在局域网可能还稍微好一点,但只要流量经过任何不可信网络,就必须上 HTTPS。最省事的方案是前面挂 Caddy 或 Nginx 做 TLS 终止,MCP 桥接服务本身继续监听 127.0.0.1。

Caddy 的配置短得感人:

mcp.example.com { reverse_proxy 127.0.0.1:8000 }

Caddy 会自动申请和续期证书,外部客户端用https://mcp.example.com/sse访问即可。Nginx 稍微啰嗦一点,但核心配置就那几行:

server { listen 443 ssl; server_name mcp.example.com; ssl_certificate /etc/letsencrypt/live/mcp.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/mcp.example.com/privkey.pem; location / { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_buffering off; proxy_read_timeout 3600s; } }

这里有个常见的 400 报错值得单独提醒:如果你只开了 443 的 HTTPS,但客户端里把地址写成了http://mcp.example.com,浏览器或 HTTP 库会收到 “The plain HTTP request was sent to HTTPS port” 的错误。遇到这种提示,先别怀疑服务端,检查客户端 URL 的 scheme 是不是 https。

4.4 第三层:网络边界收敛

认证和加密做好了,网络层面还要尽量缩小暴露面。桥接服务如果只是给办公室局域网用,监听 127.0.0.1 加反向代理就够了,流量根本不出本机来第二层。如果一定要让云服务器上的端口对公网开放,务必在安全组、防火墙里做好来源白名单:

# 只允许内网网段访问 8000 端口 sudo ufw allow from 192.168.1.0/24 to any port 8000

另外,强烈建议给 MCP 服务创建独立的低权限系统账号,目录权限只给最小范围。filesystem server 只让它读指定的工作目录,数据库 MCP 用只读账号连接。能力上做减法,永远是安全的第一原则。

4.5 一个特殊的局域网场景

如果客户端和 server 都在私网内,最安全也最省事的方式根本不是暴露公网端口,而是用私有组网工具把几台机器拉进同一个虚拟局域网,然后 MCP 服务只监听虚拟局域网网段内的地址。这样既拿到了远程调用的便利,又没有真正的公网暴露面,是目前我比较推荐的内部方案。

5. 踩坑实录:转换过程中最常见的 8 个问题

5.1 502 Bad Gateway / upstream closed

症状是客户端报 “unexpected status 502 bad gateway” 或者干脆连接中断。大部分时候不是反代或 supergateway 本身的问题,而是上游 stdio 子进程崩了。排查顺序:先看桥接服务的日志,再看子进程是否还活着,最后把--stdio里的命令手动在 shell 里跑一遍,确认它在无交互环境下不会立刻退出。

5.2 307 重定向导致 POST body 丢失

有些代理配置会把请求 307 跳转到另一个路径,而部分 HTTP 客户端在执行 307 时会误把 POST 方法降级成 GET,导致消息体丢失,客户端表现就是握手一直卡着或直接报错。应对办法是让客户端和 server 处在同一个域名路径下,或者显式关闭代理层的重定向,不要让请求被无谓地转一圈。

5.3 10 秒超时被 abort

我见过网页端调用 MCP 时疯狂报 “http service abort request for 10000ms timeout”,原因就是浏览器或某些轻量客户端的默认超时只有 10 秒。如果 MCP 工具本身就跑得久,比如数据库查询要二十秒,那前端会先等不及掐断连接。解决思路有两个方向:在客户端和代理层把超时调大,比如前面 Nginx 配置里的proxy_read_timeout 3600s;或者把耗时操作拆成“提交任务 + 查询结果”两步,不要在一个 MCP 调用里同步跑完。

5.4 Authorization 头对不上

桥接服务要求 Bearer Token,客户端配错了验证方式,就会反复 401/403。有一种特别容易误导人的报错:“remote: http basic: access denied”,这通常发生在 Git 或某些客户端默认走 Basic Auth 的场合,而 MCP 服务期望的是 Bearer 格式。排查方法很简单:在服务端临时关掉认证,确认客户端能连通,再逐步放开认证,看是头部格式问题还是 token 内容问题。

5.5 端口被悄悄占用

supergateway 启动报端口冲突很常见,尤其是开发机上 8000、3000 这类热门端口。可以用lsof -i :8000查占用进程,或者干脆换一个不常用端口,比如 18080、28080。实际部署时还可以用一个固定端口段,把 MCP 服务和普通 Web 服务隔离管理。

5.6 CORS 跨域问题

如果有一个 Web 前端页面在 5173 端口,它要调用 8000 端口的 MCP 服务,浏览器会因为跨域直接拦掉请求。supergateway 启动时加--cors "*"可以快速放通。开发环境放开无所谓,生产环境记得列白名单。这个报错浏览器控制台会写得非常明确,看 Network 面板就能定位。

5.7 子进程环境变量缺失

用 systemd 或 Docker 启动桥接服务时,stdio server 继承的环境跟你在终端里完全不一样。重点检查 PATH、NODE_PATH、HOME,很多 server 启动时还要读配置文件,配置文件路径写的是相对路径,就会在 systemd 下瞬间退出。处理方式是启动前用绝对路径、显式设置环境变量,并在日志里确认子进程确实起来了。

5.8 客户端一直转圈不报错

这种情况最折磨人。客户端既不报超时也不报错,就是工具列表加载不出来。我的经验是 90% 是 SSE 连接没建立成功。回头用 curl 的-N参数直连 SSE 端点,如果连接立刻断开,说明服务端没有正常 hold 住连接,很可能又是子进程问题或路由层把 SSE 响应当普通请求缓冲了。记得检查 Nginx 的proxy_buffering offproxy_http_version 1.1,这两个没配好,SSE 流会断得无声无息。

我把这些问题整理成速查表,方便你碰到时快速对照:

症状根因方向首要排查动作
502 网关错误stdio 子进程崩溃看服务端日志、手动跑子进程
握手卡死 / body 丢失307 重定向 + POST 降级检查代理层重定向规则
固定 10 秒超时客户端/代理超时太短调大超时或改异步任务
反复 401/403Token 格式或 header 不对curl 带 Bearer 直连测试
连接就断端口冲突或 SSE 缓冲换端口、关 proxy_buffering
跨域报错CORS 未配置启动参数加 --cors
systemd 下启动失败环境变量/PATH 缺失绝对路径、手动测试启动命令

6. 进阶方案:直接用 Python 写一个原生 HTTP MCP server

如果你准备从零写一个新服务,说实话再套一层 stdio 桥接就有点绕了。直接用 SDK 写原生 HTTP transport 更干净。我用 Python 生态的 FastMCP 举个例子,几行代码就有一个可以远程访问的 MCP server:

from fastmcp import FastMCP mcp = FastMCP("demo-server") @mcp.tool() def add(a: int, b: int) -> int: """计算两个整数之和""" return a + b if __name__ == "__main__": mcp.run(transport="http", host="127.0.0.1", port=9000)

运行起来之后,这个服务本身就提供 SSE 端点和 message 端点,客户端直接配 URL 就能用,不涉及任何进程桥接。FastMCP 底层是基于 ASGI 的,你也可以把它挂载到更大的 Web 应用里做统一鉴权、日志审计。

用 TypeScript SDK 也是类似思路,核心是把StreamableHTTPServerTransport挂到 HTTP 框架上。自己写的好处是鉴权粒度完全可控:你可以在调用特定工具前检查用户权限,可以把所有请求记录下来,甚至对返回值做脱敏。这些能力都是 supergateway 那种“黑盒桥接”给不了的。

代价是这部分工作量实实在在地落在了你身上。所以我的建议很明确:存量 server 用 supergateway,新写服务用原生 HTTP transport,不要反过来。

最后分享一个调试技巧

我个人实际跑下来最大的体会是:转 HTTP 的过程本身不出问题,出问题基本都在“环境差异”。同一套 stdio server,在终端里能跑、在 systemd 里可能瞬间退出;在自己电脑上能连、在服务器上被防火墙挡掉;在局域网正常、套上 HTTPS 反代又冒出奇怪的重定向。所以每一步改动都要做最小验证,不要一次性把桥接、反代、公网全配好再测试,那样出错了你根本不知道是哪一环的问题。

再分享一个小技巧:调试 MCP 的时候别急着打开客户端,用 curl 直连 SSE 端点,看到连接稳定挂住、再发一个 initialize 请求拿到正常 JSON 响应,就说明整个链路的核心已经通了。这个习惯帮我节省了大量和客户端配置搏斗的时间,强烈建议你也试试。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 12:27:01

SSM框架出租车管理系统实战解析:从项目结构到核心模块开发

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 12:26:55

用AI智能体构建数学建模工作流:从读题到可验证结论的完整实践

华数杯数学建模竞赛这类题目&#xff0c;很多人第一步就是打开 ChatGPT 把题目丢进去&#xff0c;等它吐出一篇“结论正确、过程漂亮”的答案。但真正参加过比赛的人都知道&#xff0c;AI 直接生成的建模方案往往存在两个问题&#xff1a;要么过于泛化&#xff0c;没有针对数据…

作者头像 李华
网站建设 2026/9/8 12:26:10

ANSYS+MATLAB车桥耦合振动仿真:两套模型与路面不平整度影响分析

公路车桥耦合振动这个话题&#xff0c;这几年在研究生论文里出现的频率越来越高&#xff0c;尤其是桥梁工程和车辆工程方向。原因很直接&#xff1a;一方面&#xff0c;桥梁在车辆荷载下的动力响应直接关系到安全评估和疲劳寿命预测&#xff1b;另一方面&#xff0c;随着高速重…

作者头像 李华
网站建设 2026/9/8 12:25:41

SuperGlue特征匹配实战:从原理到PyTorch部署与私有数据微调

简介&#xff1a;该资源是一套面向图像匹配与视觉定位场景的SuperGlue PyTorch可训练实现&#xff0c;适合具备一定深度学习基础、希望二次开发特征匹配模型的研究者与开发者。项目在官方超点实现基础上做了多项工程优化&#xff1a;支持batchsize大于1训练&#xff0c;损失计算…

作者头像 李华
网站建设 2026/9/8 12:24:28

vLLM 显存泄漏如何观测?Kvcachescope 实战剖析 KV Cache 分配与释放

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华