
先给读者一个结论如果你经常在远程 AI 客户端比如部署在云服务器上的 Agent、公司内网的 AI 应用里调用本地 MCP 服务又不想把 MCP 服务直接裸露到公网那么类似 Forth MCP 这类“桥接/代理”方案就非常值得试一试。本文会拆解 MCP 的基本概念、这类工具要解决的核心问题、一个可落地的部署流程以及过程中的高频坑和最佳实践。在写正文之前我先把一句话说明白MCPModel Context Protocol是一个让 AI 应用与外部工具、数据源对接的开放协议。而“Forth MCP”这类项目解决的是远程 AI 客户端如何安全、稳定地访问你本地机器上的 MCP Server。理解了这个目标后面的配置就不会乱。1. MCP 到底是什么为什么需要桥接很多刚开始接触 MCP 的朋友会把 MCP 和 API 网关、RPC 框架搞混。其实 MCP 的出现背景很直接AI 模型自身不具备调用外部工具的能力但通过 MCPAI 应用可以按照统一协议去发现工具、调用工具、读取工具返回的结果。1.1 MCP 协议的核心角色在 MCP 架构里通常有三个角色角色作用常见实现MCP Host发起会话的 AI 应用Claude Desktop、Cursor、自研 AgentMCP Client与 MCP Server 建立连接并传输协议数据SDK 内部的客户端组件MCP Server暴露工具、资源、提示词给 AI 应用文件读取工具、数据库查询工具、Playwright 自动化等假设你写了一个 MCP Server它提供了读取本地文件的工具。如果你的 AI 应用与这个 Server 运行在同一台机器或同一个局域网内直接通过stdio或 HTTP 端口连接即可。但现实场景往往更复杂你的 AI 客户端运行在云端而数据源、内部系统、自动化工具却放在本地开发机或公司内网。直接让云端 AI 客户端通过公网 IP 去连本地 MCP Server会带来两个问题本地机器没有固定公网地址端口映射配置繁琐。直接把 MCP 端口暴露到公网存在严重的安全风险任何人都可能调用你的工具。1.2 Forth MCP 这类工具解决什么问题Forth MCP 的核心思路是在“远端 AI 客户端”和“本地 MCP Server”之间加一层代理或隧道机制。它让远程客户端只面对一个可靠、可控的接入点而真正的 MCP Server 仍然停留在本地网络内。你可以把 Forth MCP 理解成一个“信使”远程 AI 客户端 - Forth MCP 代理 - 本地 MCP Server客户端通过代理暴露出来的统一地址发起请求代理负责把请求转发给本地的 MCP Server再把结果返回。这样既不需要把 MCP Server 直接暴露到公网又可以让远程 AI 客户端享受本地工具的完整能力。1.3 适合使用这类方案的场景本地有数据库查询工具、文件管理工具希望云端 Agent 也能调用。公司内网有一台 Windows 机器上面跑着 WinAppDriver 或 UI 自动化工具需要通过 MCP 暴露给远程 AI 编程助手。你正在调试 Figma MCP、Playwright MCP 等工具但云端 Codex、Cursor 等客户端无法直连本地环境。团队里多人共享一个 MCP 工具但工具实际运行在某位成员的开发机上。反过来如果你的 MCP Server 和 AI 客户端本来就在同一台机器上就没有必要引入桥接层直接用标准 MCP 连接方式即可。2. 环境准备与版本说明由于 Forth MCP 本身是一个社区项目不同版本的安装方式可能略有差异。这里我给出通用的环境准备思路你只要把版本替换成自己实际使用的版本即可。2.1 基础运行环境为了保证 MCP 客户端、代理、Server 三层都能正常工作建议先确认以下环境操作系统Windows 10/11、macOS 12、Ubuntu 20.04 均可。运行时Node.js 18 或 Python 3.9取决于你使用的 MCP SDK 版本。包管理器npm、pnpm 或 pip。网络要求本地机器能访问互联网远程客户端能访问代理服务器地址。IDE / 工具VS Code 或任意文本编辑器建议安装 REST Client 或 curl 用于调试。版本需要根据你的项目实际情况调整。例如如果你用的是modelcontextprotocol/sdk需要注意 SDK 在 1.x 版本后调整了不少 API 名称如果你的项目还停留在 0.x请先查看更新日志。2.2 需要一个可用的本地 MCP Server在测试 Forth MCP 之前你需要先有一个能正常工作的 MCP Server。这里我们用一个最简单的“echo 工具”作为示例。假设项目目录结构如下forth-mcp-demo/ ├── local-server/ │ ├── package.json │ └── index.js ├── forth-proxy/ │ ├── package.json │ └── proxy.js └── README.mdlocal-server放本地 MCP Serverforth-proxy放桥接代理。这样分开管理逻辑更清楚。2.3 安装依赖在local-server目录下初始化 npm 项目并安装 MCP SDKcd local-server npm init -y npm install modelcontextprotocol/sdk如果你使用的是 Python则推荐pip install mcp但下面为了演示 Node.js 流程我以 npm 安装为主。你完全可以用 Python 实现同样的逻辑MCP 协议本身是语言无关的。3. 核心原理远程 AI 客户端如何访问本地 MCP Server在配置之前建议先建立一个整体认知MCP 的传输层分为stdio和HTTP/SSE两种模式。stdioMCP Server 由客户端作为子进程启动通信走标准输入输出。这种方式适合本地调用无需网络端口。HTTP/SSEMCP Server 作为独立的 HTTP 服务运行客户端通过网络请求访问。只有这种方式适合远程连接。Forth MCP 代理要解决的就是把本地的stdio或本地 HTTP 端口转换成一个远程可访问的、包含认证和转发能力的统一入口。3.1 普通 MCP Server 的启动方式一个最简单的 MCP Server 示例local-server/index.js// 文件路径local-server/index.js import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; const server new McpServer({ name: echo-demo, version: 1.0.0 }); server.tool(echo, { message: { type: string } }, async (args) { return { content: [ { type: text, text: echo: ${args.message} } ] }; }); const transport new StdioServerTransport(); await server.connect(transport); console.error(Local MCP Server started via stdio);这个 Server 依靠stdio传输如果你想让它被远程访问还需要做一层封装。3.2 远程客户端连接本地 Server 的两种思路第一种思路直接把 MCP Server 改成 HTTP 模式监听一个本地端口然后通过端口转发工具暴露出去。// 文件路径local-server/http-server.js import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { SSEServerTransport } from modelcontextprotocol/sdk/server/sse.js; import express from express; const app express(); const server new McpServer({ name: echo-demo-http, version: 1.0.0 }); server.tool(echo, { message: { type: string } }, async (args) { return { content: [ { type: text, text: echo: ${args.message} } ] }; }); let transport; app.get(/sse, async (req, res) { transport new SSEServerTransport(/messages, res); await server.connect(transport); }); app.post(/messages, async (req, res) { if (transport) { await transport.handlePostMessage(req, res); } }); app.listen(3001, () { console.error(Local MCP HTTP Server listening on 3001); });但这里存在一个问题如果这个服务只监听127.0.0.1远程客户端连不上如果监听0.0.0.0又等于把工具暴露到局域网甚至公网非常危险。第二种思路让本地 MCP Server 保持只监听127.0.0.1再通过 Forth MCP 这类代理层对外提供接入能力。代理层负责认证、转发、日志和流量控制。这就是本文推荐的方案。3.3 Forth MCP 的工作流程从流程上看Forth MCP 启动后通常会做三件事启动一个 Agent 服务监听一个本地或远程可访问的端口。根据配置寻找指定目录下的 MCP 配置文件例如.mcp.json或者直接连接已启动的 MCP Server。当远程 AI 客户端发起 MCP 请求时Agent 将请求转发给对应的本地 MCP Server并将结果原路返回。用文字描述就是步骤 1远程 AI 客户端向 Forth MCP 的接入地址发起请求。 步骤 2Forth MCP 校验请求身份和权限。 步骤 3Forth MCP 将请求转换为本地 MCP Client 调用。 步骤 4本地 MCP Client 与本地 MCP Server 通过 stdio 或 HTTP 通信。 步骤 5返回结果给 Forth MCP再由 Forth MCP 返回给远程客户端。这个设计的好处是本地 MCP Server 不需要关心远程客户端的身份也不需要考虑公网安全策略它只信任 Forth MCP 这个“中间人”。4. 完整实战搭建一个可供远程 AI 客户端访问的 MCP 服务下面我们用一个可运行的示例走一遍从本地 MCP Server 到远程可访问的完整流程。为了安全和普适性我会尽量给出可复制的代码但大家要根据自己的 SDK 版本调整 API 名称。4.1 创建项目结构先创建目录结构mkdir forth-mcp-demo cd forth-mcp-demo mkdir local-server mkdir forth-proxy4.2 编写本地 MCP Server在local-server目录下初始化项目并安装依赖cd local-server npm init -y npm install modelcontextprotocol/sdk express然后编写index.js提供一个简单的echo工具// 文件路径local-server/index.js const { McpServer } require(modelcontextprotocol/sdk/server/mcp.js); const { StdioServerTransport } require(modelcontextprotocol/sdk/server/stdio.js); const server new McpServer({ name: local-echo-server, version: 0.1.0 }); server.tool( echo, { message: { type: string, description: 要回显的消息 } }, async ({ message }) { return { content: [ { type: text, text: [local echo] ${message} } ] }; } ); async function main() { const transport new StdioServerTransport(); await server.connect(transport); console.error(Local MCP Server is running via stdio); } main().catch((error) { console.error(error); process.exit(1); });注意这里用的是 CommonJS 的require如果你的package.json中没有设置type: module那么.js文件默认按 CommonJS 解析。上面的写法可以直接运行。验证本地 Server 是否正常node index.js启动后程序不会打印输出到标准输出而是等待 stdio 输入。只要没有报错就说明 Server 已经准备好了。4.3 编写 Forth 代理核心逻辑说明这里并不是从官方文档照搬 Forth MCP 的源码而是给你一个可用的代理实现思路。如果你使用的是 Forth MCP 官方包可以直接用它的 CLI 命令原理是一致的。在forth-proxy目录下初始化项目cd ../forth-proxy npm init -y npm install express cors编写proxy.js实现以下功能提供 HTTP 接口接收远程客户端发来的 JSON 请求。通过环境变量读取本地 MCP Server 的启动命令。使用child_process.spawn启动本地 MCP Server 的子进程。将客户端请求写入子进程的 stdin从 stdout 读取结果并返回。这是一个简化版本旨在展示“代理转发”的核心思想// 文件路径forth-proxy/proxy.js const express require(express); const cors require(cors); const { spawn } require(child_process); const app express(); app.use(cors()); app.use(express.json()); app.post(/mcp, (req, res) { const { method, params } req.body || {}; // 1. 启动本地 MCP Server 子进程 const child spawn(node, [./local-server/index.js], { cwd: process.env.LOCAL_SERVER_DIR || ./local-server, stdio: [pipe, pipe, pipe] }); let output ; let errorOutput ; child.stdout.on(data, (data) { output data.toString(); }); child.stderr.on(data, (data) { errorOutput data.toString(); }); // 2. 构造 MCP 初始化请求 const initRequest { jsonrpc: 2.0, id: req.body.id || 1, method: initialize, params: { protocolVersion: 2024-11-05, capabilities: {}, clientInfo: { name: forth-proxy, version: 1.0.0 } } }; child.stdin.write(JSON.stringify(initRequest) \n); // 3. 等待一段时间后再发送真实请求 // 实际项目中需要更完善的初始化握手和缓冲处理 setTimeout(() { const toolRequest { jsonrpc: 2.0, id: (req.body.id || 1) 1, method: tools/call, params: { name: echo, arguments: { message: params?.message || hello } } }; child.stdin.write(JSON.stringify(toolRequest) \n); }, 500); // 4. 收集结果并返回 setTimeout(() { child.kill(); res.json({ jsonrpc: 2.0, result: { content: [ { type: text, text: output.trim() || 错误信息: ${errorOutput} } ] } }); }, 2000); }); app.listen(process.env.PORT || 3000, () { console.error(Forth proxy listening on ${process.env.PORT || 3000}); });这个示例比较粗糙但它演示了代理的基本工作方式代理不需要自己实现复杂的 MCP 语义只需要把请求转发给本地 MCP 子进程。实际生产环境里应该使用正式的 MCP Client SDK 来管理连接和协议生命周期。4.4 使用官方 MCP Client 改进代理更规范的写法是在代理进程里同时扮演 MCP Client 角色。这样你不需要手动解析 JSON-RPC。安装modelcontextprotocol/sdknpm install modelcontextprotocol/sdk改进后的proxy-client.js// 文件路径forth-proxy/proxy-client.js const express require(express); const cors require(cors); const { Client } require(modelcontextprotocol/sdk/client/index.js); const { StdioClientTransport } require(modelcontextprotocol/sdk/client/stdio.js); const app express(); app.use(cors()); app.use(express.json()); async function callLocalMCP(inputMessage) { const transport new StdioClientTransport({ command: node, args: [./local-server/index.js], cwd: process.env.LOCAL_SERVER_DIR || ./local-server }); const client new Client({ name: forth-proxy-client, version: 1.0.0 }); await client.connect(transport); const result await client.request( { method: tools/call, params: { name: echo, arguments: { message: inputMessage } } }, { timeout: 10000 } ); await client.close(); return result; } app.post(/mcp, async (req, res) { try { const message req.body?.params?.message || hello; const result await callLocalMCP(message); res.json(result); } catch (error) { res.status(500).json({ error: error.message }); } }); app.listen(process.env.PORT || 3000, () { console.error(Forth proxy with MCP client listening on ${process.env.PORT || 3000}); });这里我们没有直接操作 JSON-RPC而是用了官方 SDK 的 Client 和 StdioClientTransport。tools/call请求会通过子进程发送给本地 MCP Server返回值再通过 HTTP 返回给远程调用方。4.5 配置远程 AI 客户端现在你的代理已经监听在3000端口。假设你打算让云端 AI 客户端通过这个代理来访问本地工具需要让代理服务器地址能被远程客户端访问。如果代理运行在本地开发机但远程客户端在云上你有几种选择将代理部署到一台公网服务器上并通过安全方式访问本地 MCP Server。使用内网穿透或云厂商的安全隧道注意必须正规、合法且经过授权。在团队内网部署远程客户端与代理处于同一内网。注意无论使用哪种方式都必须加上身份验证不能直接把/mcp接口暴露给所有人。为了简单测试你可以先在本机用 curl 验证代理curl -X POST http://127.0.0.1:3000/mcp \ -H Content-Type: application/json \ -d {method:tools/call,params:{message:hello from remote}}预期返回一个包含[local echo] hello from remote的 JSON 结构。4.6 添加简单的 Token 鉴权在真实环境中代理必须校验请求来源。这里演示一个最基础的 Bearer Token 校验// 在 proxy-client.js 中加入中间件 app.use((req, res, next) { const authHeader req.headers.authorization || ; const token authHeader.replace(Bearer , ); if (token ! process.env.ACCESS_TOKEN) { return res.status(401).json({ error: unauthorized }); } next(); });启动时设置环境变量ACCESS_TOKENyour-secret-token PORT3000 node proxy-client.js这样远程 AI 客户端在请求时需要在 Header 中带上Authorization: Bearer your-secret-token。5. 常见问题与排查思路在实际搭这套方案时我遇到过不少坑。下面整理几个高频问题按“现象 - 原因 - 解决方案”的顺序展开。5.1 本地 MCP Server 启动成功但代理请求超时问题现象常见原因解决思路请求代理接口时一直转圈最终 timeout本地 MCP Server 初始化握手没有完成检查initialize请求是否已发送在代理代码中等待initialized通知后再调用工具代理返回空内容stdout 解析时机太早使用 SDK Client 而不是手动解析输出或者增加缓冲并等待完整 JSON 输出子进程直接退出命令路径或参数错误用child.stderr输出调试信息确认工作目录是否正确如果你的代理是用spawn手动实现的最容易出问题的是 MCP 协议必须经过initialize、initialized、tools/list等握手步骤直接写tools/call会被 Server 拒绝。这也是为什么我更推荐用官方 SDK Client它已经封装好了握手逻辑。5.2 远程 AI 客户端连接时提示“工具注册不上”这个问题在 Figma MCP、Playwright MCP 等社区工具上特别常见。现象是远程客户端能访问代理但找不到任何工具。排查顺序先确认本地 MCP Server 是否真正暴露了工具。用tools/list请求手动查询工具列表。检查代理返回的响应格式是否符合客户端预期。可能的原因有本地 MCP Server 没有正确注册工具可能因为参数 schema 写错。代理层只做了tools/call转发没有处理tools/list。客户端要求 MCP Server 返回 capabilities但代理没有透传。如果是用官方 SDK Client可以通过client.listTools()来验证const tools await client.listTools(); console.log(tools);确保工具列表非空后再检查远程客户端配置。5.3 代理服务可以访问但只能本机访问如果app.listen(3000)默认监听的是所有网卡那本机外也能访问。但如果你显式写了app.listen(3000, 127.0.0.1)则只有本机可以请求。有些云服务器有防火墙或安全组即使代理监听0.0.0.0外网依然无法访问。需要检查云厂商安全组是否放行对应端口。本地防火墙是否阻止了入站连接。代理进程是否使用了 Docker 且端口映射是否正确。再次强调不要为了省事直接把端口放行到公网除非你加了强认证并且明确知道自己在做什么。5.4 提示 JSON-RPC 方法不存在如果远程客户端或代理代码里使用了tools/call但本地 MCP Server 返回“Method not found”通常是因为 SDK 版本差异。MCP 协议仍在演进中不同版本的 SDK 对方法名、参数结构、协议版本的兼容策略不同。建议统一本地 MCP Server、代理 SDK、远程客户端的 MCP 协议版本。查看各组件日志中的实际请求方法名。尝试使用默认协议版本避免手动指定不兼容的版本。6. 最佳实践与工程建议把上面的流程跑通之后接下来要考虑的是稳定性和安全性。下面几条建议都来自实际项目中的教训。6.1 本地 MCP Server 与代理分层不要把代理逻辑和 MCP Server 逻辑全写在一个进程里。按目录拆开forth-mcp-demo/ ├── local-server/ # 本地 MCP 工具实现 ├── forth-proxy/ # 统一接入层、认证、日志 └── shared/ # 共享的类型定义和配置这样本地工具可以独立升级代理层可以单独加限流、审计不影响业务工具。6.2 使用环境变量管理敏感配置不要把 Token、API Key、路径写在代码里。使用.env文件或环境变量注入PORT3000 ACCESS_TOKENyour-secret-token LOCAL_SERVER_DIR/data/forth-mcp/local-server MCP_PROTOCOL_VERSION2024-11-05同时在.gitignore中忽略.env文件。6.3 为代理增加访问日志和请求审计远程 AI 客户端调用本地工具时应当记录调用时间、客户端 IP。调用的工具名称。请求参数摘要。返回状态和耗时。这样一旦出现安全问题或者某个工具被误调用能快速追溯。日志不要记录完整敏感参数尤其是数据库连接串、密码、Token 等。6.4 不要让代理处于“无状态”状态如果你用spawn每次请求都启动一个新的本地 MCP Server 子进程会导致资源浪费和大量握手开销。更合理的方式是启动常驻的本地 MCP ServerHTTP 模式。代理进程通过 MCP Client SDK 连接这个 Server连接后复用。为每个远程会话维护独立的 Client 实例避免串数据。但这会提升实现复杂度。如果只是小范围使用每次启动子进程也可以但一定要限制并发数量并设置超时。6.5 网络传输安全远程客户端和代理之间的通信强烈建议使用 HTTPS。如果你是自建服务可以通过 Nginx 或 Caddy 终结 TLS如果使用云厂商的负载均衡开启 HTTPS 证书也很方便。代理本身应只监听内网地址或 localhost外层由 Nginx 反向代理。示例 Nginx 配置片段server { listen 443 ssl; server_name mcp.example.com; ssl_certificate /etc/nginx/cert.pem; ssl_certificate_key /etc/nginx/key.pem; location /mcp { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Authorization $http_authorization; } }6.6 权限边界设计不要把所有的本地工具一股脑透传出去。建议在代理层维护一个“允许调用名单”只有名单内的工具才能被远程调用。const ALLOWED_TOOLS process.env.ALLOWED_TOOLS ? process.env.ALLOWED_TOOLS.split(,) : [echo]; app.post(/mcp, async (req, res) { const toolName req.body?.params?.name; if (toolName !ALLOWED_TOOLS.includes(toolName)) { return res.status(403).json({ error: tool ${toolName} is not allowed }); } // 继续处理 });这样即使本地 MCP Server 暴露了数据库删除工具也不会被远程 AI 客户端随意调用。6.7 监控与告警如果你的代理服务于生产环境建议增加健康检查接口/healthz返回当前代理进程和本地 MCP Server 的连接状态。请求成功率、延迟指标接入 Prometheus 或简单的定时 curl 告警。日志轮转避免代理日志占用过多磁盘。7. 总结与下一步学习路线通过本文你应该已经掌握了几个关键点MCP 协议中 Host、Client、Server 三者的关系。本地 MCP Server 为什么不能被远程 AI 客户端直接访问。Forth MCP 这类桥接/代理方案的工作原理。使用 Node.js 实现一个带 Token 鉴权的 MCP 代理的完整过程。常见连接失败、工具注册不上、超时等问题的排查方法。如果你打算深入学习可以继续沿着这几个方向延伸阅读 MCP 协议文档中的initialize、tools/list、resources/list、prompts/list等核心方法。学习如何将本地 MCP Server 从 stdio 切换到 HTTP/SSE 模式。研究云厂商的托管 MCP 网关以及它们与自建代理的异同。尝试为你的代理加入 WebSocket 支持让 AI 客户端能进行流式交互。在实际项目中请优先关注安全边界远程访问本地工具能力虽然方便但也意味着攻击面扩大。每次修改代理配置时都应遵循最小权限原则先在测试环境验证再逐步放开访问范围。如果你已经实际运行过一个本地 MCP Server不妨现在就把它接到代理层试点一下从最简单的echo工具开始逐步替换成真正有价值的生产工具。只有亲手跑通一遍才会对这些协议细节有更深的体会。