本地 MCP 服务想给远程模型调用?用 cpolar 做一个临时 HTTPS 入口

📅 2026/7/9 8:35:03 👁️ 阅读次数 📝 编程学习
本地 MCP 服务想给远程模型调用?用 cpolar 做一个临时 HTTPS 入口

本地 MCP 服务想给远程模型调用?用 cpolar 做一个临时 HTTPS 入口

最近调 MCP 工具时,我遇到一个很典型的问题:服务在本机跑得好好的,模型一旦换成远程测试环境,就完全调用不到。

这个问题不是 MCP 本身复杂,而是网络边界很现实。你电脑上的127.0.0.1:8000只对你自己可见,远程模型、云端 Agent 平台、同事的测试环境都不在这台机器上,它们看到的localhost也不是你的电脑。

所以这篇文章只解决一件事:本地跑一个最小 MCP 风格的 HTTP/SSE 工具服务,用 cpolar 给它生成临时 HTTPS 入口,再把这个入口交给远程模型或测试环境调用。重点会放在可达性、鉴权、只读边界、日志排查和关闭隧道后的安全收口。

为什么本地 MCP 服务远程不可达

很多 MCP 服务一开始都是为本地客户端准备的。比如你在电脑上启动一个工具服务,然后让本机的 IDE、桌面客户端、调试脚本去调用它。这种场景里,监听127.0.0.1很合理,因为调用方就在同一台机器上。

但远程模型不一样。只要调用方跑在云端,它就无法直接访问你的回环地址,也通常访问不到你家里或办公室路由器后面的内网地址。即使你把服务监听到0.0.0.0,云端也不会自动穿过 NAT、防火墙和运营商网络打进来。

这也是不少人调 MCP 时误判的地方:本地curl成功,只能证明服务活着,不能证明远程可达。远程调用失败时,可能是公网入口没有配置,也可能是 HTTPS 不符合平台要求,或者鉴权头、路径、请求方法写错。

cpolar 在这里适合做一个临时入口。它把你本地某个端口映射成公网 HTTPS 地址,外部请求先到 cpolar 提供的地址,再转发回你的电脑。它不替你实现 MCP,也不替你做权限控制,只负责打通“远程能访问到本地端口”这一步。

我的建议是:只在开发和验收窗口打开隧道,结束后马上关闭;本地工具尽量只提供查询类能力,不把删除、写库、发消息这类高风险动作暴露出去。

先起一个最小工具服务

为了把链路讲清楚,下面不用完整 MCP SDK 起步,而是先写一个 MCP 风格的 HTTP/SSE 工具服务。它包含三个能力:健康检查、工具列表、工具调用,再加一个简单 SSE 端点用来观察连接。

新建目录:

mkdir mcp-cpolar-demo cd mcp-cpolar-demo python -m venv .venv source .venv/bin/activate pip install fastapi uvicorn sse-starlette

创建main.py

import asyncio import json import os from datetime import datetime from fastapi import FastAPI, Header, HTTPException, Request from sse_starlette.sse import EventSourceResponse app = FastAPI(title="Local MCP Tool Demo") MCP_TOKEN = os.getenv("MCP_TOKEN", "change-me-dev-token") def check_token(authorization: str | None) -> None: expected = f"Bearer {MCP_TOKEN}" if authorization != expected: raise HTTPException(status_code=401, detail="invalid token") @app.get("/health") async def health(): return {"ok": True, "time": datetime.now().isoformat()} @app.get("/mcp/tools") async def list_tools(authorization: str | None = Header(default=None)): check_token(authorization) return { "tools": [ { "name": "read_project_summary", "description": "Read a short local project summary. This demo is read-only.", "input_schema": { "type": "object", "properties": { "project": {"type": "string"} }, "required": ["project"], }, } ] } @app.post("/mcp/call") async def call_tool( request: Request, authorization: str | None = Header(default=None), ): check_token(authorization) body = await request.json() tool_name = body.get("name") arguments = body.get("arguments") or {} print("\n========== MCP CALL ==========") print("time:", datetime.now().isoformat()) print("client:", request.client.host if request.client else "unknown") print("headers:", json.dumps(dict(request.headers), ensure_ascii=False, indent=2)) print("body:", json.dumps(body, ensure_ascii=False, indent=2)) if tool_name != "read_project_summary": raise HTTPException(status_code=404, detail="tool not found") project = arguments.get("project", "demo") return { "content": [ { "type": "text", "text": f"Project {project}: local read-only MCP tool is reachable.", } ] } @app.get("/sse") async def sse(authorization: str | None = Header(default=None)): check_token(authorization) async def events(): yield {"event": "ready", "data": "local MCP SSE channel connected"} while True: await asyncio.sleep(10) yield {"event": "heartbeat", "data": datetime.now().isoformat()} return EventSourceResponse(events())

启动服务:

export MCP_TOKEN='replace-with-a-long-random-token' uvicorn main:app --host 127.0.0.1 --port 8000

这里继续绑定127.0.0.1。原因很简单:我只希望服务被本机进程访问,再由 cpolar 负责外部入口。如果直接监听局域网地址,排查范围会变大,也更容易把临时工具暴露给同网段的其它设备。

本机先验证健康检查:

curl http://127.0.0.1:8000/health

再验证工具列表:

curl http://127.0.0.1:8000/mcp/tools \ -H "Authorization: Bearer replace-with-a-long-random-token"

最后调用一次工具:

curl -X POST http://127.0.0.1:8000/mcp/call \ -H "Authorization: Bearer replace-with-a-long-random-token" \ -H "Content-Type: application/json" \ -d '{"name":"read_project_summary","arguments":{"project":"mcp-demo"}}'

如果终端能看到MCP CALL日志,说明本地服务本身没有问题。后面远程打不进来,就优先检查 cpolar 地址、HTTPS、路径和请求头。

用 cpolar 映射一个 HTTPS 地址

本地端口确认可用后,再打开 cpolar 隧道。假设你的本地服务端口是8000

cpolar http 8000

启动后,控制台会显示一个公网访问地址,通常类似:

https://xxxxxx.cpolar.top -> http://localhost:8000

后面配置远程模型或测试环境时,就不要再填http://127.0.0.1:8000,而是填这个 HTTPS 地址。例如:

MCP_BASE_URL=https://xxxxxx.cpolar.top MCP_TOOLS_URL=https://xxxxxx.cpolar.top/mcp/tools MCP_CALL_URL=https://xxxxxx.cpolar.top/mcp/call MCP_SSE_URL=https://xxxxxx.cpolar.top/sse

先从自己电脑上访问公网地址,确认 cpolar 转发正常:

curl https://xxxxxx.cpolar.top/health curl https://xxxxxx.cpolar.top/mcp/tools \ -H "Authorization: Bearer replace-with-a-long-random-token"

这一步很关键。公网地址在你本机能访问,不代表远程平台一定配置正确,但至少能证明 cpolar 隧道和本地服务之间是通的。

远程模型或测试环境怎么配置

不同平台对 MCP 的接入方式不一样。有的平台配置的是 MCP server URL,有的平台配置的是工具网关地址,还有的平台需要你在 Agent 代码里写一个 HTTP client。无论形式怎么变,核心配置基本就是三类:入口地址、鉴权头、工具调用路径。

如果你有一个远程测试脚本,可以这样写:

import os import requests BASE_URL = os.environ["MCP_BASE_URL"].rstrip("/") TOKEN = os.environ["MCP_TOKEN"] headers = {"Authorization": f"Bearer {TOKEN}"} tools = requests.get(f"{BASE_URL}/mcp/tools", headers=headers, timeout=10) print("tools:", tools.status_code, tools.text) payload = { "name": "read_project_summary", "arguments": {"project": "remote-test"}, } result = requests.post( f"{BASE_URL}/mcp/call", headers={**headers, "Content-Type": "application/json"}, json=payload, timeout=30, ) print("call:", result.status_code, result.text)

远程环境里设置:

export MCP_BASE_URL='https://xxxxxx.cpolar.top' export MCP_TOKEN='replace-with-a-long-random-token' python remote_test.py

如果你接的是某个 Agent 平台,配置思路也一样:

  • Base URL 填 cpolar 的 HTTPS 地址;
  • 工具列表路径填/mcp/tools或平台要求的发现端点;
  • 工具调用路径填/mcp/call或你自己的 MCP gateway 路由;
  • Header 增加Authorization: Bearer ...
  • SSE 场景填https://xxxxxx.cpolar.top/sse

这里不要把 token 写在 URL 查询参数里。查询参数更容易出现在平台日志、浏览器历史、代理日志里。放在 Header 里虽然也不是绝对安全,但比裸露在 URL 上更好管理。

鉴权和只读边界要先做

临时 HTTPS 入口最容易被低估的风险,是“临时”两个字会让人放松。只要地址还有效,它就是公网入口。别人如果拿到了 URL,就可能尝试请求你的本地服务。

所以我会给本地 MCP 工具加三层限制。

第一层是强制鉴权。哪怕只是调试,也要要求Authorization。token 用长随机字符串,不要用123456dev-token这种容易猜的值。

第二层是工具只读。库存查询、项目摘要、日志读取、配置查看这类能力可以作为演示工具;删除文件、执行 shell、修改数据库、发送消息这类能力尽量不要放进临时隧道。如果必须做写操作,也要增加二次确认和参数白名单。

第三层是缩小数据范围。比如工具只允许读取某个目录下的摘要文件,只返回脱敏后的结果,不把.env、密钥、用户数据、完整日志直接回传给模型。

上面的示例里,read_project_summary故意只返回一段固定文本,就是为了强调这个边界。真实项目可以换成“读取指定项目状态”“查询构建结果”“返回最近一次任务摘要”,但不要一上来就给远程模型一个万能本地操作入口。

日志排查:先看有没有打到本机

远程调用失败时,我一般按这个顺序排查。

第一,看远程平台返回的状态码。如果是401,大概率是 token 没带、前缀不是Bearer、或者环境变量和本地服务不一致。如果是404,优先检查路径,比如/mcp/call写成了/mcp/calls。如果是405,通常是 GET/POST 方法用错了。

第二,看本地 uvicorn 终端有没有请求日志。只要请求真正转发到了本机,终端里通常会出现访问记录,工具调用还会打印MCP CALL。如果平台显示超时,但本地完全没有日志,问题多半在公网 URL、cpolar 隧道状态、平台出网限制或 HTTPS 校验上。

第三,用公网地址自己打一遍。比如:

curl -v https://xxxxxx.cpolar.top/health curl -v https://xxxxxx.cpolar.top/mcp/tools \ -H "Authorization: Bearer replace-with-a-long-random-token"

curl -v能看到 TLS、重定向、连接耗时和响应头。很多时候,一眼就能发现填错了域名、用了过期地址,或者平台要求 HTTPS 但你填成了 HTTP。

第四,确认请求体格式。远程模型工具调用经常会把参数包装成不同结构,有的叫arguments,有的叫input,有的直接把 JSON Schema 参数平铺在 body 里。服务端最好在日志里打印原始 body,确认平台实际发来的内容,再做适配。

SSE 连接单独测

如果你的 MCP 接入使用 SSE,建议单独测连接,不要和工具调用混在一起排查。

本机测试:

curl -N http://127.0.0.1:8000/sse \ -H "Authorization: Bearer replace-with-a-long-random-token"

公网测试:

curl -N https://xxxxxx.cpolar.top/sse \ -H "Authorization: Bearer replace-with-a-long-random-token"

正常情况下,你会先看到ready事件,之后每隔一段时间看到心跳。SSE 的问题通常集中在三类:平台不支持长连接、代理中间层超时、鉴权头没有带到 SSE 请求上。

如果平台只支持普通 HTTP 工具调用,那就不要强行上 SSE。先用/mcp/tools/mcp/call把工具调用跑通,等链路稳定后再切换到平台支持的 MCP 传输方式。

验收完成后立刻关闭隧道

这套方案适合临时调试,不适合把本地开发机长期当生产服务挂在公网。

验收完成后,至少做这几件事:

  • 停掉 cpolar 进程,让公网地址失效;
  • 停掉本地 uvicorn 服务;
  • 在远程平台删除或禁用这条临时 MCP 配置;
  • 轮换本次调试用过的 token;
  • 检查本地日志里有没有敏感请求体,必要时清理;
  • 把真实服务配置切回正式环境或内网测试环境。

如果后续还要经常调试,可以把启动命令写进脚本,但不要把 token 明文提交到仓库。更稳妥的方式是从环境变量读取 token,并在每次调试前生成新的随机值。

例如:

export MCP_TOKEN="$(openssl rand -hex 32)" uvicorn main:app --host 127.0.0.1 --port 8000

另一个终端打开:

cpolar http 8000

这样每次调试都是新的 token、新的临时窗口,结束后也更容易收口。

写在最后

本地 MCP 服务给远程模型调用,卡点往往不在“工具怎么写”,而在“远程到底能不能访问到本机”。先用一个最小 HTTP/SSE 工具服务验证链路,再用 cpolar 提供临时 HTTPS 入口,是一个比较轻量的办法。

但这条链路要有边界:只在调试窗口开放,只暴露必要路径,只给只读工具,所有请求都带鉴权,日志里能看清楚远程实际发了什么。调试结束后关闭隧道、轮换 token、清理平台配置。

这样用,cpolar 更像一个临时验收通道:帮你快速确认远程模型能调用本地 MCP 工具,同时不把开发机长期暴露在公网风险里。