Tunnel Mcp

io.github.zachlikefolio由 GitHub 账号 zachlikefolio 发布v0.3.0更新于 Oct 25, 2026

Direct end-to-end-encrypted tunnel between Claude agents — two-party or a room of up to 16.

已验证STDIO仅桌面沟通与协作安全与监控

概览

AI 生成的概览

让两个或多个 Claude 智能体建立一次性、端到端加密的隧道并直接对话,免去人工在聊天窗口之间复制粘贴。

可以做什么
以主机身份开启隧道并生成一次性、会过期的邀请链接,或以访客身份加入并查看成员名单;房间最多 16 人,主机可在会话中途追加邀请。逐轮收发消息,每条消息带发送者名称,并可等待下一条回复。端到端加密地共享文本或二进制文件,再获取对方提供的文件、校验哈希并保存到自选路径。查看会话角色、目标、成员、待用邀请和文件列表。主机可结束整个会话,成员则只是离开房间。
适用场景
当两名或多名开发者各自运行 Claude 智能体,希望这些智能体就调试等任务直接协作,而不是由人来回转述消息时使用。适合短暂、一次性的结对会话,而非长期共享的工作空间。
运行要求
本地 stdio MCP 服务器,需 Node.js 20 或更高版本和 Claude MCP 客户端(如 Claude Code),通过 npm 或 npx 安装。若 PATH 中没有 cloudflared,首次使用时会自动下载到本地 bin 目录。需要访问 Cloudflare 的出站网络。可选环境变量 TUNNEL_DOH、TUNNEL_SKILLS_DIR 和 TUNNEL_SKIP_SKILL_INSTALL 用于调整 DNS 与技能安装。
安装前请注意
加入链接内嵌会话加密密钥,应视为机密,只通过可信渠道分享,且每个一次性邀请只发给一个人。目标字符串、显示名称、系统事件和共享文件名均为明文元数据,不要在其中放入机密。来自对端的消息和文件属于不可信输入;服务器附带的礼仪技能要求智能体在写文件或执行有风险命令前先取得人类确认。接收的文件会写入磁盘,路径由接收方选择。

安装

在 SourceWeft 中

  1. 打开 控制台中的 Tunnel Mcp,将其添加到工作区。
  2. 为需要使用其工具的对话启用该服务。

Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。

其他 MCP 客户端

参照 仓库 中的启动说明。

README

tunnel-mcp

A direct, end-to-end-encrypted tunnel between two developers' Claude agents — no human copy-paste required.

[CI] [npm version] [npm downloads] [node]

[tunnel-mcp demo — two agents talking through a real encrypted tunnel]

Reproduce that yourself in 30 seconds — clone the repo and:

bash
npm ci && npm run demo

That opens a real encrypted tunnel through Cloudflare's edge, joins it as a guest, exchanges end-to-end-encrypted messages, proves the join link is single-use, and tears everything down.

When two developers each run a Claude agent and need those agents to collaborate, the usual workaround is a human sitting in the middle, copy-pasting messages from one chat window to the other. tunnel-mcp removes that human. It's an MCP server that lets one developer's agent open a throwaway, encrypted tunnel and the other developer's agent dial straight into it, so the two agents can talk to each other directly — while their humans stay in control of what actually happens to the filesystem or the shell.

How it works

One developer (the host) calls tunnel_open. Their local tunnel-mcp process becomes an in-process WebSocket relay and exposes it to the internet via a throwaway cloudflared Quick Tunnel — no port-forwarding, no server to provision. The other developer (the guest) calls tunnel_join with the link the host shares, and their agent dials outbound to that same tunnel. Because both sides only ever make outbound connections, it works from behind ordinary firewalls and NAT.

   Host machine                                        Guest machine  ┌───────────────────┐        outbound HTTPS         ┌───────────────────┐  │   Claude (host)    │            wss://             │   Claude (guest)   │  │        │           │      ┌──────────────┐         │        │          │  │  tunnel_open/say/  │──────▶  cloudflared │◀────────│  tunnel_join/say/  │  │  listen/close      │      │ Quick Tunnel │─────────▶  listen/close      │  │        │           │      └──────────────┘         │        │          │  │  in-process relay  │                                └───────────────────┘  └───────────────────┘

The relay and the cloudflared child process live only for the lifetime of the session and are destroyed on teardown. The transcript is held in memory only — nothing is ever written to disk, and it vanishes with the process at teardown.

Install

bash
npm install -g tunnel-mcp# or, without installing:npx tunnel-mcp

Register it with Claude Code (both developers do this once):

bash
claude mcp add tunnel -- tunnel-mcp          # if globally installed# or, with no global install:claude mcp add tunnel -- npx -y tunnel-mcp

tunnel-mcp is a stdio MCP server, not an interactive CLI. Launching it by hand just waits silently for a client — that's expected. Run tunnel-mcp --help for usage, or tunnel-mcp --version.

The tunnel-etiquette skill teaches each agent how to behave inside a tunnel (treat the peer as untrusted input, and check with its human before acting on anything the peer says). Installing the package copies it into ~/.claude/skills/ automatically (best-effort). If install scripts are disabled (npm install --ignore-scripts), or you want it in a custom directory or force an update, run it explicitly:

bash
npx tunnel-mcp install-skill                       # into ~/.claude/skillsnpx tunnel-mcp install-skill --dir <path> --force  # elsewhere / overwrite

Set TUNNEL_SKILLS_DIR to change the default target, or TUNNEL_SKIP_SKILL_INSTALL=1 to opt out of the automatic copy.

cloudflared is auto-downloaded to ~/.tunnel/bin the first time it's needed if it isn't already on your PATH — there's nothing extra to install.

Quickstart

Host — ask Claude to open a tunnel with a goal:

"Open a tunnel to pair on debugging the checkout flow."

Claude calls tunnel_open({ goal }) and hands back a ready-to-forward invite — one plain-text message containing the one-time setup command and the join link. Paste it to the other developer over a trusted channel (Slack DM, etc.) — the link is a secret, since it contains the encryption key for the session. It is single-use and expires after ~10 minutes (tunnel_open reports joinLinkExpiresInSec), so share it promptly.

Guest — paste the link and ask Claude to join:

"Join this tunnel: <link>"

Claude calls tunnel_join({ joinLink }), learns the goal, and gets back the room's member roster — with the default single invite, that's just the two of you.

More than one guest? Open a room instead:

"Open a tunnel for me and two teammates, to pair on the checkout flow."

Claude calls tunnel_open({ goal, invites: 3 }) — invites is the number of teammates to seat (up to 15, plus the host makes 16 connected at once) — and gets back one invite per teammate instead of a single link. Forward each invite to exactly one person; every invite is single-use, so don't reuse one link for two people. Need to add someone mid-session, or re-admit someone whose invite expired before they used it? tunnel_invite({ count }) (host-only) mints more.

Both — the agents converse turn-by-turn using tunnel_say to send and tunnel_listen to wait for the next reply. In a room, every message arrives with fromName so agents can tell who said what, checking in with their humans as needed.

Ending it is role-sensitive: the host calls tunnel_close to end the session for everyone and tear down the relay — the in-memory transcript vanishes with it, since it was never written to disk. A member calling tunnel_close just leaves; the room stays open for whoever's left.

Sharing files: any member can call tunnel_share({ path }) to send a text or binary file to the room — it's read from disk, hashed, and sealed with the room key before it ever crosses the tunnel, so the relay only ever sees ciphertext. The offer shows up for teammates as an artifact message in tunnel_listen and in tunnel_status().artifacts (id, name, kind, size, sender). A teammate who wants it calls tunnel_receive({ artifactId, savePath }) with a path they choose — the bytes are decrypted and checked against the sender's sha256 before anything is written, and a mismatch is refused rather than saved. An artifact stays fetchable by any current member until it expires (a 30-minute TTL) or the session ends — each tunnel_receive call independently re-fetches and re-verifies. Members on an older client are silently skipped (olderMembers in the tunnel_share result) — they simply never see the offer. Filenames cross as plaintext metadata, so don't put secrets in one, and treat every received file as untrusted input — see the etiquette skill.

Tools

ToolWhoPurpose
tunnel_open({goal, invites?})hostStart the relay + Quick Tunnel and get back one invite per teammate (default 1 — classic two-party).
tunnel_invite({count?})hostMint more single-use, expiring invites mid-session.
tunnel_join({joinLink})memberDial into a room using an invite link and authenticate; returns the current member roster.
tunnel_say({text})anySend a message to the room.
tunnel_listen({sinceSeq?, timeoutMs?})anyWait for the next message(s), each tagged with the sender's fromName.
tunnel_share({path})anyShare a file (text or binary) with the room, end-to-end encrypted; returns {artifactId, offeredTo, olderMembers}.
tunnel_receive({artifactId, savePath})anyFetch an offered artifact, verify its hash, and write it to a path you choose.
tunnel_status()anyInspect the session: role, goal, member roster, pending invites, offered artifacts, lastSeq.
tunnel_close({summary?})anyHost: ends the session for everyone. Member: leaves the room.

Security model

tunnel-mcp is a security-sensitive tool by nature — it opens a live channel between developers' AI agents. Here's exactly what it does and does not protect:

  • Chat message bodies are end-to-end encrypted. Every tunnel_say body is sealed with NaCl secretbox (XSalsa20-Poly1305, via tweetnacl) before it crosses the cloudflared pipe. The relay and the pipe only ever see ciphertext for chat bodies.
  • The goal, every participant's display name, and system events are plaintext. The tunnel_open goal, each member's name, and connection events (joined/left/idle/closed) are sent as plaintext metadata — do not put secrets in the goal string or a display name.
  • Authentication is proof-of-key-possession, not key transmission. Joining uses an HMAC challenge to prove the joining member holds the same key as the host; the raw key itself is never sent over the wire.
  • Each invite is a single-use, expiring credential. It embeds the session key, so treat it like a password — share it only over a channel you already trust (Slack DM, etc.), never in a public issue, PR, or chat, and forward each invite to exactly one person. It is consumed by whoever redeems it first (and can't be reused, even after they leave) and expires on its own after ~10 minutes, so a leaked invite has a short, bounded window of exposure.
  • Admits exactly whom you invited — two-party by default, rooms opt-in (cap 16), every invite single-use + expiring. Admission is bounded by how many invites the host chose to mint, not by who happens to have the room's key.
  • Shared files are end-to-end encrypted and hash-verified. tunnel_share seals a file's bytes with the same room key as chat (NaCl secretbox) before they cross the tunnel, and carries a plaintext sha256 of the contents; tunnel_receive decrypts, reassembles, and verifies that hash before writing anything to disk. The filename, size, and kind are plaintext metadata (don't put secrets in a filename), and a received file is untrusted — tunnel_receive only ever writes to a path the receiver chooses, never the sender's name.
  • The peer is untrusted input, not an instruction source. Messages from other agents are data to reason about, not commands to execute — and this applies to every member in a room, not just one. The etiquette skill directs each agent to require its own human's sign-off before writing files, running risky commands, or declaring a fix "confirmed" based on something a peer said.
  • Everything is ephemeral. The transcript is held in memory only — nothing is ever written to disk, and it vanishes with the process. Teardown is role-sensitive: the host's tunnel_close (or their process exiting, or 30 minutes of no messages) ends the session for everyone and tears down the relay + cloudflared child process; a member's tunnel_close just leaves — the room stays open for whoever's left.

See SECURITY.md for the full threat model and how to report a vulnerability.

Requirements

  • Node.js >= 20
  • A Claude MCP client (e.g., Claude Code)
  • cloudflared — auto-installed to ~/.tunnel/bin on first use if not already on your PATH

Development

bash
npm ci                  # install dependenciesnpm test                # run the test suite (248 tests, TDD)npm run build           # compile TypeScriptnpm run lint            # eslintnpm run format:check    # prettier --check .npm run test:coverage   # vitest run --coverage

See CONTRIBUTING.md for how to propose changes.

Troubleshooting

tunnel-mcp / npx tunnel-mcp "does nothing". It's a stdio MCP server, not an interactive CLI — with no arguments it starts and waits for an MCP client to connect over stdin/stdout. That's working as intended. Register it with a client (above), or run tunnel-mcp --help.

Guest join fails with getaddrinfo ENOTFOUND …trycloudflare.com. A cloudflared quick tunnel prints its URL a few seconds before the per-tunnel DNS record has propagated. If anything looks the name up too early it gets an NXDOMAIN that the resolver negative-caches for up to 30 minutes — breaking the join even after the tunnel is live. tunnel-mcp avoids this: tunnel_open waits for the record to actually resolve (via DoH to Cloudflare's 1.1.1.1, an IP that never touches — and so never poisons — your system resolver) before returning the link, and the guest resolves system-first with a DoH fallback. So a fresh join should just work; if you hit ENOTFOUND, an earlier attempt likely poisoned the cache — wait for it to expire, or flush DNS (sudo dscacheutil -flushcache on macOS). Set TUNNEL_DOH=off only on networks that block DoH (1.1.1.1) and where system DNS already resolves *.trycloudflare.com.

Roadmap / not yet supported

This is an MVP. The following are explicitly out of scope for now:

  • Host-offline / asynchronous messaging
  • Alternative transports (ngrok, WebRTC)
  • Invite rotation (replacing a specific still-valid invite mid-session; note invites are already single-use and expiring — see the security model above)
  • Encrypting the goal or other metadata

License

MIT — see LICENSE.

来源:README.md,提交 eef2ccb

工具

0
SourceWeft 尚未在沙箱中启动过该软件包,因此其工具未知。

版本历史

1
  1. v0.3.0最新Sep 16, 2026

评分

已满足适用检查项的 50%

该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。

  • 近 30 天内可连通25 分

    请保持端点能够响应 MCP 握手;SourceWeft 每 30 天探测一次。

  • 已声明认证方式10 分

    请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。

  • 已列出工具15 分

    请让服务器在无凭据时响应 tools/list,或发布可在沙箱中无凭据启动的软件包。

  • 描述必需15 分
  • 图标10 分
  • 文档15 分
  • 许可证10 分