Selaa lähdekoodia

Merge pull request #1953 from anthropics/add-plugin/mcp-tunnels

Add mcp-tunnels plugin
Den Delimarsky 3 kuukautta sitten
vanhempi
sitoutus
6cc16f4b16

+ 11 - 0
.claude-plugin/marketplace.json

@@ -1481,6 +1481,17 @@
       "category": "development",
       "homepage": "https://github.com/anthropics/claude-plugins-official/tree/main/plugins/mcp-server-dev"
     },
+    {
+      "name": "mcp-tunnels",
+      "description": "Connect Claude to a private MCP server through an Anthropic MCP tunnel. The /create-docker-mcp-tunnel command drives the Docker Compose quickstart end to end: certificates, proxy config, cloudflared, and a verifiable sample server.",
+      "author": {
+        "name": "Anthropic",
+        "email": "support@anthropic.com"
+      },
+      "source": "./plugins/mcp-tunnels",
+      "category": "development",
+      "homepage": "https://github.com/anthropics/claude-plugins-official/tree/main/plugins/mcp-tunnels"
+    },
     {
       "name": "mercadopago",
       "description": "Mercado Pago full-product integration toolkit. One agent routes to four orchestration skills (mp-integrate wizard, mp-webhooks, mp-test-setup, mp-review) that pull every endpoint, payload, and snippet live from the official Mercado Pago MCP server. The MCP must always be connected — there is no offline mode.",

+ 8 - 0
plugins/mcp-tunnels/.claude-plugin/plugin.json

@@ -0,0 +1,8 @@
+{
+  "name": "mcp-tunnels",
+  "description": "Connect Claude to a private MCP server through an Anthropic MCP tunnel. Drives the Docker Compose quickstart end to end: certificates, proxy config, cloudflared, and a verifiable sample server.",
+  "author": {
+    "name": "Anthropic",
+    "email": "support@anthropic.com"
+  }
+}

+ 202 - 0
plugins/mcp-tunnels/LICENSE

@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 122 - 0
plugins/mcp-tunnels/README.md

@@ -0,0 +1,122 @@
+# mcp-tunnels
+
+Connect Claude to an MCP server running inside your private network through an
+Anthropic [**MCP tunnel**](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview)
+— no inbound ports, no public exposure, no IP allowlisting on your origin.
+Traffic flows over an outbound-only connection.
+
+> **Research preview.** MCP tunnels is provided "as-is" with no uptime or
+> support commitment and depends on a third-party transport provider
+> (Cloudflare). Review the
+> [security model](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/security)
+> before sending anything sensitive.
+
+## Commands
+
+### `/create-docker-mcp-tunnel [deployment-dir]`
+
+Drives the MCP tunnels
+[**quickstart**](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/quickstart)
+end to end on your machine, using Docker
+Compose with manually supplied credentials (the shortest path for local
+testing). It walks you through the parts only you can do in the Claude Console
+and runs everything else for you:
+
+1. **Preflight** — checks Docker, Docker Compose, OpenSSL, and outbound
+   connectivity.
+2. **Create the tunnel** (Console) — you create it and copy the domain; the
+   token stays out of the chat and goes into a locked-down, gitignored `.env`.
+3. **Certificates** — generates a CA and a server certificate with OpenSSL,
+   with the exact extensions the tunnel requires.
+4. **Register the CA** (Console) — you upload `ca.crt`; the tunnel goes Active.
+5. **Upstream** — scaffolds a verifiable FastMCP sample server, or wires up an
+   MCP server you already have.
+6. **Proxy config + Compose** — writes `mcp-proxy.yaml` and a
+   `docker-compose.yaml` with digest-pinned images and the cloudflared agent.
+7. **Start and verify** — brings the stack up and checks the proxy and tunnel
+   logs.
+8. **Call it from Claude** — shows you how to reach the server from Managed
+   Agents and the Messages API.
+
+It also carries a troubleshooting matrix (TLS handshake failures, the
+`routes`-must-be-a-map gotcha, the `tls.key` permission issue, the
+config-is-not-hot-reloaded trap, upstream IP validation) and the operational
+basics for token rotation and certificate renewal.
+
+**Usage:**
+
+```
+/create-docker-mcp-tunnel
+/create-docker-mcp-tunnel ~/work/my-tunnel
+```
+
+### Copying the CA certificate to another machine
+
+You register the CA in the Console from a browser, which is often a different
+machine than the one running the stack (for example, the tunnel runs in a
+remote homespace but you upload `ca.crt` from your laptop or devbox). Only the
+**certificate** (`<deployment-dir>/data/ca.crt`, ~1 KB PEM) leaves the host —
+never `data/ca.key` or `data/tls.key`.
+
+For a file this small, the simplest path is to print it and paste it into the
+Console's certificate field directly:
+
+```bash
+cat <deployment-dir>/data/ca.crt   # default: ~/mcp-tunnel/data/ca.crt
+```
+
+To copy it as a file with `scp`, run the command from whichever machine can
+SSH to the other (`scp` can't relay between two remotes). Pulling from a
+homespace onto your devbox — if you've run `coder config-ssh`, the host is
+`coder.<workspace>`:
+
+```bash
+scp coder.<workspace>:<deployment-dir>/data/ca.crt .
+# generic form: scp <homespace-ssh-host>:~/mcp-tunnel/data/ca.crt .
+```
+
+Or push from the host to the devbox, if the host can reach it:
+
+```bash
+scp <deployment-dir>/data/ca.crt <user>@<devbox-host>:~/
+```
+
+## What gets built
+
+A small container stack on your host:
+
+| Container | Role |
+|---|---|
+| **mcp-proxy** | Anthropic's proxy. Terminates inner TLS with a cert you control, validates upstream IPs, routes by hostname. |
+| **cloudflared** | The tunnel agent. Outbound-only to the Anthropic tunnel edge; shares the proxy's network namespace. |
+| **hello-mcp** *(optional)* | A FastMCP sample server, only if you don't have an MCP server to expose yet. |
+
+When it's running, the routed server is reachable from Claude at
+`https://<subdomain>.<your-tunnel-domain>/<path>` with nothing listening on a
+public port.
+
+## Requirements
+
+- Docker and Docker Compose.
+- OpenSSL 1.1.1 or newer.
+- A Claude Console role that can manage MCP tunnels.
+- Outbound access to `api.anthropic.com:443` and the tunnel edge on 7844
+  TCP/UDP. No inbound ports are opened.
+
+## Scope and next steps
+
+This plugin targets the **manual-credentials, single-host, local-testing**
+path. For a hardened single-host deployment (non-root, read-only rootfs,
+dropped capabilities), a Kubernetes deployment, or programmatic access via
+[Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation),
+see the official deployment guides:
+[Deploy with Docker Compose](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-compose) /
+[Deploy with Helm](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-helm).
+
+## Author
+
+Anthropic (support@anthropic.com)
+
+## License
+
+See `LICENSE`.

+ 369 - 0
plugins/mcp-tunnels/commands/create-docker-mcp-tunnel.md

@@ -0,0 +1,369 @@
+---
+description: Stand up an Anthropic MCP tunnel locally with Docker Compose so Claude can call a private MCP server (manual-credentials quickstart).
+argument-hint: "[deployment-dir] (default: ./mcp-tunnel)"
+allowed-tools: [Bash, Read, Write, Edit, AskUserQuestion]
+---
+
+# Create a Docker MCP tunnel
+
+Drive the
+[**MCP tunnels quickstart**](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/quickstart)
+end to end: from zero to Claude calling a private MCP server through an
+Anthropic-operated tunnel, using Docker Compose with manually supplied
+credentials (the shortest path for local testing).
+
+> MCP tunnels is in **research preview**. It is provided "as-is" with no uptime
+> or support commitment and depends on a third-party transport (Cloudflare).
+> Do not put production traffic through this without reading the
+> [security model](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/security).
+
+You are guiding the user through a mix of **local commands you run** and
+**Console actions only they can do** (creating the tunnel, uploading the CA).
+Be a careful operator: explain each step briefly, run the commands, check the
+output, and stop with a clear diagnosis if something fails.
+
+Deployment directory: use `$ARGUMENTS` if the user passed a path, otherwise
+default to `./mcp-tunnel`. Refer to it below as `$DIR`.
+
+## What you'll build
+
+A container stack on the user's machine:
+
+- **mcp-proxy** — Anthropic's proxy. Terminates the inner TLS handshake using
+  a certificate the user controls, validates upstream IPs, routes by hostname.
+- **cloudflared** — the tunnel agent. Outbound-only connection to the Anthropic
+  tunnel edge; shares the proxy's network namespace.
+- **hello-mcp** *(optional)* — a sample FastMCP server, only if the user has no
+  MCP server of their own to expose yet.
+
+When it's up, the routed server is reachable from Claude at
+`https://<subdomain>.<tunnel-domain>/<path>` with nothing listening on a public
+port.
+
+## Step 0 — Preflight
+
+Run these and report what's missing before going further:
+
+```bash
+docker --version && docker compose version && openssl version
+```
+
+- Docker + Docker Compose are required. `openssl` 1.1.1+ is required (the
+  commands below use `-addext`, available in 1.1.1+).
+- Confirm the host has **outbound** access to `api.anthropic.com:443` and the
+  tunnel edge (`198.41.192.0/19`, `2606:4700:a0::/44`) on **7844 TCP and UDP**.
+  No inbound ports are opened.
+
+If `docker compose` (v2) is unavailable but `docker-compose` (v1) exists, use
+that and tell the user; the compose file is v2-compatible.
+
+## Step 1 — Create the tunnel (Console — user action)
+
+Tell the user to do this in the [Claude Console](https://console.anthropic.com)
+(see [Create a tunnel](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#create-a-tunnel)):
+
+1. Sidebar → **Manage → MCP tunnels** → **New tunnel**. Give it a name.
+2. Leave **Set up programmatic access** **off** — this quickstart uses manual
+   credentials.
+3. Open the tunnel. From the **Connection** section copy two values:
+   - **Domain** — looks like `abcd1234.tunnel.anthropic.com`
+   - **Token** — click the eye icon, then copy
+
+Then ask the user, via AskUserQuestion or a direct prompt, for the **Domain**.
+**Do not ask them to paste the Token into the chat.** The token is a secret
+that authenticates the outbound tunnel connection; keep it out of the
+transcript. Instead, tell them you will create a `$DIR/.env` file and they
+should paste the token into it themselves (Step 3), or have them export it:
+`export TUNNEL_TOKEN='eyJ...'` in the shell you'll run compose from.
+
+Record the domain as `TUNNEL_DOMAIN` for the steps below.
+
+## Step 2 — Deployment directory
+
+```bash
+mkdir -p "$DIR"/{config,data}
+cd "$DIR"
+```
+
+## Step 3 — Credentials file
+
+Create `$DIR/.env` (compose auto-loads it; this survives reboots, unlike a
+shell `export`). Write `TUNNEL_DOMAIN` yourself; leave a placeholder for the
+secret and have the **user** fill it in:
+
+```
+TUNNEL_DOMAIN=<the domain from step 1>
+TUNNEL_TOKEN=PASTE_TUNNEL_TOKEN_HERE
+```
+
+Then lock it down and make sure it never gets committed:
+
+```bash
+chmod 600 "$DIR/.env"
+printf '.env\ndata/\n' > "$DIR/.gitignore"
+```
+
+Pause and have the user replace `PASTE_TUNNEL_TOKEN_HERE` with the real token
+(tell them the exact file path). Verify it's set without printing it:
+
+```bash
+cd "$DIR" && grep -q '^TUNNEL_TOKEN=eyJ' .env && echo "token looks set" || echo "token NOT set — edit .env"
+```
+
+Load it for the openssl/config steps in this shell:
+
+```bash
+cd "$DIR" && set -a && . ./.env && set +a && echo "domain: $TUNNEL_DOMAIN"
+```
+
+## Step 4 — Generate the CA and server certificate
+
+The proxy terminates an inner TLS handshake using a certificate signed by a CA
+the user controls. Generate both (Linux/macOS shown; the
+[quickstart](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/quickstart)
+also has a Windows PowerShell variant — offer it if the user is on Windows):
+
+```bash
+cd "$DIR"
+
+openssl req -x509 -newkey rsa:2048 -nodes \
+  -keyout data/ca.key -out data/ca.crt \
+  -days 3650 -subj "/CN=mcp-tunnel-ca" \
+  -addext "basicConstraints=critical,CA:TRUE" \
+  -addext "keyUsage=critical,keyCertSign,cRLSign" \
+  -addext "subjectKeyIdentifier=hash"
+
+cat > data/tls.ext <<EOF
+subjectAltName = DNS:${TUNNEL_DOMAIN},DNS:*.${TUNNEL_DOMAIN}
+authorityKeyIdentifier = keyid,issuer
+extendedKeyUsage = serverAuth
+EOF
+
+openssl req -newkey rsa:2048 -nodes \
+  -keyout data/tls.key -out /tmp/server.csr \
+  -subj "/CN=${TUNNEL_DOMAIN}"
+openssl x509 -req -in /tmp/server.csr \
+  -CA data/ca.crt -CAkey data/ca.key -CAcreateserial \
+  -out data/tls.crt -days 90 -extfile data/tls.ext
+
+chmod 644 data/tls.key
+```
+
+Why these flags: the explicit `-addext` extensions make the CA satisfy the
+tunnel's [certificate requirements](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/reference#certificate-requirements)
+regardless of distro `openssl.cnf` defaults;
+`-extfile` (not `-copy_extensions`, which is OpenSSL 3.0+ only) keeps this
+working on OpenSSL 1.1.x and adds the `AuthorityKeyIdentifier` the proxy
+requires. `chmod 644 data/tls.key` is **required**: openssl writes the key
+`0600` but the proxy container runs as a non-root user and must read it.
+
+`data/tls.key` and `data/ca.key` are sensitive — they live under `data/`,
+which the `.gitignore` from Step 3 already excludes.
+
+## Step 5 — Register the CA (Console — user action)
+
+Have the user, on the tunnel detail page, scroll to **Certificates** →
+**Add certificate**
+(see [Add a CA certificate](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#add-a-ca-certificate)),
+and upload `$DIR/data/ca.crt` (or paste its contents —
+print it with `cat data/ca.crt` so they can copy it). The tunnel status flips
+to **Active** once a certificate is registered. The tunnel will not appear in
+the agent picker until this is done.
+
+Wait for the user to confirm the tunnel shows **Active** before continuing.
+
+## Step 6 — Choose the upstream MCP server
+
+Ask the user (AskUserQuestion):
+
+- **"I have an MCP server already"** — get its reachable address as
+  `scheme://host:port` (port mandatory, no path — the proxy rejects a path in
+  the upstream value at config load). It must be reachable from the proxy
+  container and resolve to an RFC1918 private address (`10/8`, `172.16/12`,
+  `192.168/16`); the proxy refuses public/loopback upstreams by default
+  (SSRF protection). If it runs as a Compose service, add it to the compose
+  file so it shares the network. If it runs on the host, see Troubleshooting
+  ("host process"). Pick a route subdomain with the user (e.g. `wiki`).
+- **"Use the sample server"** — scaffold the FastMCP `hello-server` below as a
+  Compose service `hello-mcp` and route subdomain `echo`.
+
+### Sample server (only if chosen)
+
+Write `$DIR/hello_server.py`:
+
+```python
+from mcp.server.fastmcp import FastMCP
+
+mcp = FastMCP("hello-server", host="0.0.0.0", port=9000)
+
+
+@mcp.tool()
+def hello(name: str = "world") -> str:
+    """Say hello to someone."""
+    return f"Hello, {name}!"
+
+
+if __name__ == "__main__":
+    mcp.run(transport="streamable-http")
+```
+
+## Step 7 — Proxy config
+
+Write `$DIR/config/mcp-proxy.yaml`. `tunnel_domain` is **required** (the
+proxy strips it from the incoming hostname to find the subdomain in `routes`).
+`routes` is a **flat map** subdomain → upstream URL, *not* a list:
+
+```yaml
+listen_addr: ":8080"
+log_level: info
+tunnel_domain: <TUNNEL_DOMAIN>
+tls:
+  cert_file: /data/tls.crt
+  key_file: /data/tls.key
+routes:
+  echo: http://hello-mcp:9000
+```
+
+Substitute the real `TUNNEL_DOMAIN`. Replace the `routes:` block with the
+user's chosen subdomain → upstream if they brought their own server (e.g.
+`wiki: http://wiki-mcp.internal:8080`). You can keep multiple routes.
+
+## Step 8 — Compose file
+
+Write `$DIR/docker-compose.yaml`. Images are pinned by digest:
+
+```yaml
+services:
+  mcp-proxy:
+    image: us-docker.pkg.dev/anthropic-public-registry/images/mcp-proxy@sha256:6b9adedbf2763143ec72f106ecaf0ce7fd3294e89b208f54a1db97a33d14c5ba
+    command: ["-config", "/etc/mcp-proxy/config.yaml"]
+    volumes:
+      - ./config/mcp-proxy.yaml:/etc/mcp-proxy/config.yaml:ro
+      - ./data:/data:ro
+    restart: unless-stopped
+
+  cloudflared:
+    image: cloudflare/cloudflared@sha256:6b599ca3e974349ead3286d178da61d291961182ec3fe9c505e1dd02c8ac31b0
+    command: tunnel --no-autoupdate run --url http://localhost:8080
+    environment:
+      - TUNNEL_TOKEN
+    network_mode: "service:mcp-proxy"
+    restart: unless-stopped
+```
+
+`--url http://localhost:8080` is **required** in the manual flow: no ingress
+rules are pushed server-side, so without it cloudflared 503s every request.
+`network_mode: "service:mcp-proxy"` shares the proxy's netns so
+`localhost:8080` reaches it. `environment: - TUNNEL_TOKEN` (no value) passes
+the variable through from `.env`.
+
+If the sample server was chosen, append the service:
+
+```yaml
+  hello-mcp:
+    image: python:3.13-slim
+    working_dir: /app
+    volumes:
+      - ./hello_server.py:/app/hello_server.py:ro
+    command: sh -c "pip install --quiet mcp && python hello_server.py"
+    restart: unless-stopped
+```
+
+If the user brought their own server *and* it's containerized, add its service
+here too so it shares the Compose network with the proxy.
+
+(For a hardened single-host deployment — non-root user, read-only rootfs,
+`cap_drop: ALL`, `no-new-privileges` — point the user at
+[Deploy with Docker Compose](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-compose);
+this quickstart keeps it minimal for fast local testing.)
+
+## Step 9 — Start and verify
+
+```bash
+cd "$DIR" && docker compose up -d
+sleep 5
+docker compose logs mcp-proxy | grep -i "route configured"
+docker compose logs cloudflared | grep -i "Registered tunnel connection"
+```
+
+Expect one `route configured` line per route and **four**
+`Registered tunnel connection` lines. Containers take a few seconds; rerun the
+log greps if they come back empty (don't conclude failure on the first empty
+result). If they stay empty, go to Troubleshooting.
+
+## Step 10 — Call it from Claude
+
+Tell the user both options:
+
+**Managed Agents (Console):** **Managed Agents → Sessions** → new session →
+agent picker **Create new agent** → **+ MCP Server** → select the tunnel →
+**Subdomain** = the route (`echo`), **Path** = `mcp` (FastMCP
+`streamable-http` serves at `/mcp`). Then ask: *"Use the hello tool to greet
+tunnel."* — expect a tool call and its result.
+
+**Messages API:** the host is `<subdomain>.<tunnel-domain>`; the path is
+whatever the upstream serves (`/mcp` for FastMCP). Use an API key for the
+workspace the tunnel was created in.
+
+```bash
+curl https://api.anthropic.com/v1/messages \
+  -H "Content-Type: application/json" \
+  -H "x-api-key: $ANTHROPIC_API_KEY" \
+  -H "anthropic-version: 2023-06-01" \
+  -H "anthropic-beta: mcp-client-2025-11-20" \
+  -d "{
+    \"model\": \"claude-opus-4-7\",
+    \"max_tokens\": 1024,
+    \"mcp_servers\": [{\"type\": \"url\", \"name\": \"echo\", \"url\": \"https://echo.${TUNNEL_DOMAIN}/mcp\"}],
+    \"tools\": [{\"type\": \"mcp_toolset\", \"mcp_server_name\": \"echo\"}],
+    \"messages\": [{\"role\": \"user\", \"content\": \"call hello with name=tunnel\"}]
+  }"
+```
+
+The tunnel carries encrypted traffic but does **not** authenticate to the
+upstream. If the upstream MCP server requires its own auth, the user supplies
+it the same as for any other MCP server.
+
+## Troubleshooting (diagnose in this order)
+
+| Symptom | Cause | Fix |
+|---|---|---|
+| Caller sees HTTP 500; cloudflared logs `No ingress rules were defined` | cloudflared has no local target | Ensure `--url http://localhost:8080` and `network_mode: "service:mcp-proxy"` are both present, then `docker compose up -d` |
+| Proxy exits `cannot unmarshal !!seq into map[string]string` | `routes` written as a YAML list | Use `routes: { name: http://host:port }`, not a list of objects |
+| Proxy exits `open /data/tls.key: permission denied` | key is `0600`, proxy runs non-root | `chmod 644 data/tls.key` |
+| Proxy logs `no route for host` (caller gets `502 No route configured for host`) | `tunnel_domain` missing or wrong | Set it to the exact domain on the tunnel detail page; then **restart the proxy** (next row) |
+| Edited config but nothing changed | proxy does **not** hot-reload `config.yaml` (only `tls.cert_file`) | `docker compose restart mcp-proxy` — `up -d` alone won't recreate it on a file-content change |
+| `tls handshake failed ... unknown certificate authority` | CA not registered/revoked on this tunnel | Re-upload `data/ca.crt` in the Console (Step 5) |
+| `tls handshake failed ... bad certificate` | server cert SAN ≠ `*.<tunnel-domain>`, or expired | Regenerate the server cert (Step 4) with the correct `TUNNEL_DOMAIN` |
+| `IP validation failed: <ip> is not a private address` | upstream resolves outside RFC1918 (e.g. `127.0.0.1`, public IP) | Run the upstream as a Compose service on the proxy's network; or narrow `upstream.allowed_ips` deliberately (avoid `0.0.0.0/0` outside local testing) |
+| `dial tcp ...: connect: connection refused` for `host.docker.internal` | rootless Docker can't reach the host netns | Run the MCP server as a Compose service instead of a host process |
+| HTTP 502, no `request started` in proxy log | cloudflared hadn't finished registering, or rolling update | Wait for ×4 `Registered tunnel connection` and retry |
+| Tunnel missing from agent **+ MCP Server** picker | no active certificate, or wrong workspace | Register a CA cert (Step 5); open the session in the tunnel's workspace |
+| `curl https://<proxy>:8080` fails `wrong version number` | expected — listener is plaintext WS, TLS is inside the WS stream | Don't curl the proxy directly; verify via Managed Agent or Messages API |
+
+`docker compose logs cloudflared` (token/edge reachability) and
+`docker compose logs mcp-proxy` (config/cert/routing) are the two primary
+diagnostics. Check the outbound connection first, then the inner TLS handshake,
+then upstream routing. See
+[Troubleshooting](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/troubleshooting)
+for additional cases.
+
+## Operational notes (mention briefly, don't run unprompted)
+
+- **Token rotation:** Console → **Rotate token** invalidates the old token
+  immediately. Update `TUNNEL_TOKEN` in `.env` and
+  `docker compose up -d cloudflared`.
+- **Cert renewal:** the server cert is valid 90 days. Re-sign with the same CA
+  (the registered CA doesn't change) and replace `data/tls.crt`; the proxy
+  polls and reloads it, no restart needed.
+- **Config changes always need** `docker compose restart mcp-proxy`.
+
+## Wrap up
+
+Summarize: deployment dir, route(s) configured, tunnel domain, and the exact
+URL Claude reaches the server at. Remind the user the token is a live secret in
+`$DIR/.env` (chmod 600, gitignored) and that this is a research-preview,
+local-testing setup — point them at
+[Deploy with Docker Compose](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-compose) /
+[Deploy with Helm](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-helm)
+for a hardened or programmatic-access deployment.