tunnel-mcp 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -11,6 +11,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  - Nothing yet.
13
13
 
14
+ ## [0.1.9] - 2026-07-02
15
+
16
+ ### Added
17
+
18
+ - **`tunnel_open` now returns `invite`** — a ready-to-forward plain-text message
19
+ containing the one-time setup command (`claude mcp add tunnel -- npx -y
20
+ tunnel-mcp`) and the join link, so the host's human can paste one message to
21
+ the other developer instead of explaining the install. The tool description
22
+ directs the agent to relay it verbatim.
23
+
24
+ ## [0.1.8] - 2026-07-02
25
+
26
+ ### Added
27
+
28
+ - `mcpName` (`io.github.zachlikefolio/tunnel-mcp`) in `package.json`, the
29
+ ownership-verification field required to list the server in the official
30
+ MCP Registry (registry.modelcontextprotocol.io). No functional changes.
31
+
14
32
  ## [0.1.7] - 2026-07-01
15
33
 
16
34
  ### Security
@@ -163,7 +181,9 @@ install-skill` copies the `tunnel-etiquette` skill into `~/.claude/skills`
163
181
  declaring a fix "confirmed".
164
182
  - Test suite of 109 tests built with vitest, developed test-first (TDD).
165
183
 
166
- [Unreleased]: https://github.com/zachlikefolio/tunnel-mcp/compare/v0.1.7...HEAD
184
+ [Unreleased]: https://github.com/zachlikefolio/tunnel-mcp/compare/v0.1.9...HEAD
185
+ [0.1.9]: https://github.com/zachlikefolio/tunnel-mcp/compare/v0.1.8...v0.1.9
186
+ [0.1.8]: https://github.com/zachlikefolio/tunnel-mcp/compare/v0.1.7...v0.1.8
167
187
  [0.1.7]: https://github.com/zachlikefolio/tunnel-mcp/compare/v0.1.6...v0.1.7
168
188
  [0.1.6]: https://github.com/zachlikefolio/tunnel-mcp/compare/v0.1.5...v0.1.6
169
189
  [0.1.5]: https://github.com/zachlikefolio/tunnel-mcp/compare/v0.1.4...v0.1.5
package/README.md CHANGED
@@ -4,8 +4,21 @@
4
4
 
5
5
  [![CI](https://github.com/zachlikefolio/tunnel-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/zachlikefolio/tunnel-mcp/actions/workflows/ci.yml)
6
6
  [![npm version](https://img.shields.io/npm/v/tunnel-mcp)](https://www.npmjs.com/package/tunnel-mcp)
7
+ [![npm downloads](https://img.shields.io/npm/dm/tunnel-mcp)](https://www.npmjs.com/package/tunnel-mcp)
7
8
  ![node](https://img.shields.io/badge/node-%3E%3D20-brightgreen)
8
9
 
10
+ ![tunnel-mcp demo — two agents talking through a real encrypted tunnel](docs/demo.gif)
11
+
12
+ **Reproduce that yourself in 30 seconds** — clone the repo and:
13
+
14
+ ```bash
15
+ npm ci && npm run demo
16
+ ```
17
+
18
+ That opens a real encrypted tunnel through Cloudflare's edge, joins it as a
19
+ guest, exchanges end-to-end-encrypted messages, proves the join link is
20
+ single-use, and tears everything down.
21
+
9
22
  When two developers each run a Claude agent and need those agents to collaborate,
10
23
  the usual workaround is a human sitting in the middle, copy-pasting messages from
11
24
  one chat window to the other. **tunnel-mcp** removes that human. It's an MCP
@@ -83,11 +96,12 @@ it isn't already on your `PATH` — there's nothing extra to install.
83
96
 
84
97
  > "Open a tunnel to pair on debugging the checkout flow."
85
98
 
86
- Claude calls `tunnel_open({ goal })` and returns a join link. Share that link
87
- with the other developer over a trusted channel (Slack DM, etc.) — **it's a
88
- secret**, since it contains the encryption key for the session. The link is
89
- **single-use and expires after ~10 minutes** (`tunnel_open` reports
90
- `joinLinkExpiresInSec`), so share it promptly.
99
+ Claude calls `tunnel_open({ goal })` and hands back a ready-to-forward
100
+ **invite** one plain-text message containing the one-time setup command and
101
+ the join link. Paste it to the other developer over a trusted channel (Slack
102
+ DM, etc.) — **the link is a secret**, since it contains the encryption key for
103
+ the session. It is **single-use and expires after ~10 minutes**
104
+ (`tunnel_open` reports `joinLinkExpiresInSec`), so share it promptly.
91
105
 
92
106
  **Guest** — paste the link and ask Claude to join:
93
107
 
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The ready-to-forward invite a host's human sends to the other developer.
3
+ * Every tunnel needs a second dev with tunnel-mcp installed — so the invite
4
+ * carries the one-time setup command alongside the join link, making each
5
+ * session recruit its own second participant with zero friction.
6
+ */
7
+ export declare function buildInvite(opts: {
8
+ goal: string;
9
+ joinLink: string;
10
+ expiresInSec: number;
11
+ }): string;
package/dist/invite.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The ready-to-forward invite a host's human sends to the other developer.
3
+ * Every tunnel needs a second dev with tunnel-mcp installed — so the invite
4
+ * carries the one-time setup command alongside the join link, making each
5
+ * session recruit its own second participant with zero friction.
6
+ */
7
+ export function buildInvite(opts) {
8
+ const mins = Math.max(1, Math.ceil(opts.expiresInSec / 60));
9
+ return [
10
+ `You're invited to a Claude-agent tunnel — goal: "${opts.goal}"`,
11
+ ``,
12
+ `1) One-time setup (skip if you already have tunnel-mcp):`,
13
+ ` claude mcp add tunnel -- npx -y tunnel-mcp`,
14
+ `2) Then tell your Claude:`,
15
+ ` Join this tunnel: ${opts.joinLink}`,
16
+ ``,
17
+ `The link is single-use and expires in ~${mins} minute${mins === 1 ? '' : 's'}.`,
18
+ ].join('\n');
19
+ }
package/dist/session.d.ts CHANGED
@@ -32,6 +32,7 @@ export declare class TunnelSession {
32
32
  joinLink: string;
33
33
  status: string;
34
34
  joinLinkExpiresInSec: number;
35
+ invite: string;
35
36
  }>;
36
37
  join(joinLink: string, guestName: string): Promise<{
37
38
  tunnelId: string;
package/dist/session.js CHANGED
@@ -7,6 +7,7 @@ import { GuestClient } from './relay/guestClient.js';
7
7
  import { ensureCloudflared as realEnsure } from './cloudflared/provision.js';
8
8
  import { startCloudflared as realStart } from './cloudflared/tunnelProcess.js';
9
9
  import { DEFAULT_LISTEN_TIMEOUT_MS, DEFAULT_IDLE_TEARDOWN_MS, DEFAULT_JOIN_LINK_TTL_MS, OPEN_RETRY_ATTEMPTS, } from './config.js';
10
+ import { buildInvite } from './invite.js';
10
11
  const DEFAULT_DEPS = {
11
12
  ensureCloudflared: realEnsure,
12
13
  startCloudflared: (bin, port) => realStart(bin, port),
@@ -76,11 +77,13 @@ export class TunnelSession {
76
77
  void this.close();
77
78
  });
78
79
  relay.submitLocal(buildSystem('host', `tunnel opened — goal: ${goal}`));
80
+ const joinLinkExpiresInSec = Math.round(joinTtlMs / 1000);
79
81
  return {
80
82
  tunnelId,
81
83
  joinLink,
82
84
  status: 'waiting_for_guest',
83
- joinLinkExpiresInSec: Math.round(joinTtlMs / 1000),
85
+ joinLinkExpiresInSec,
86
+ invite: buildInvite({ goal, joinLink, expiresInSec: joinLinkExpiresInSec }),
84
87
  };
85
88
  }
86
89
  async join(joinLink, guestName) {
package/dist/tools.js CHANGED
@@ -28,7 +28,7 @@ function register(server, name, schema, cb) {
28
28
  }
29
29
  export function registerTools(server, session, opts) {
30
30
  register(server, 'tunnel_open', {
31
- description: 'Open a tunnel as host and get a join link to share. The link is a secret — share it over a trusted channel. It is single-use (works for exactly one guest) and expires (see joinLinkExpiresInSec in the result), so tell the human to share it promptly.',
31
+ description: 'Open a tunnel as host and get a join link to share. The link is a secret — share it over a trusted channel. It is single-use (works for exactly one guest) and expires (see joinLinkExpiresInSec in the result), so tell the human to share it promptly. The result includes `invite`: a ready-to-forward plain-text message with the one-time setup command and the join link — show it to your human verbatim so they can paste it straight to the other developer.',
32
32
  inputSchema: { goal: z.string() },
33
33
  }, async ({ goal }) => ok(await session.open(goal, opts.displayName)));
34
34
  register(server, 'tunnel_join', {
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "tunnel-mcp",
3
- "version": "0.1.7",
3
+ "mcpName": "io.github.zachlikefolio/tunnel-mcp",
4
+ "version": "0.1.9",
4
5
  "description": "Let two developers' Claude agents talk directly through an ephemeral, end-to-end-encrypted tunnel.",
5
6
  "type": "module",
6
7
  "bin": {
@@ -30,6 +31,7 @@
30
31
  "format:check": "prettier --check .",
31
32
  "dev": "tsx src/index.ts",
32
33
  "e2e": "tsx scripts/e2e-two-agents.ts",
34
+ "demo": "tsx scripts/demo.ts",
33
35
  "postinstall": "node postinstall.mjs",
34
36
  "prepublishOnly": "npm run build"
35
37
  },
@@ -66,14 +68,14 @@
66
68
  },
67
69
  "devDependencies": {
68
70
  "@anthropic-ai/sdk": "^0.109.0",
69
- "@types/node": "^20.14.0",
71
+ "@types/node": "^26.1.0",
70
72
  "@types/ws": "^8.5.10",
71
73
  "@vitest/coverage-v8": "^4.1.9",
72
- "eslint": "^9.9.0",
74
+ "eslint": "^10.6.0",
73
75
  "fast-check": "^4.8.0",
74
76
  "prettier": "^3.3.0",
75
77
  "tsx": "^4.16.0",
76
- "typescript": "^5.5.0",
78
+ "typescript": "^6.0.3",
77
79
  "typescript-eslint": "^8.0.0",
78
80
  "vitest": "^4.1.9"
79
81
  }