diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index eae181ce..fe2d39ba 100644 --- a/.claude-plugin/marketplace.json +++ b/.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.", diff --git a/plugins/mcp-tunnels/.claude-plugin/plugin.json b/plugins/mcp-tunnels/.claude-plugin/plugin.json new file mode 100644 index 00000000..97c30328 --- /dev/null +++ b/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" + } +} diff --git a/plugins/mcp-tunnels/LICENSE b/plugins/mcp-tunnels/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/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. diff --git a/plugins/mcp-tunnels/README.md b/plugins/mcp-tunnels/README.md new file mode 100644 index 00000000..d1a93ebb --- /dev/null +++ b/plugins/mcp-tunnels/README.md @@ -0,0 +1,115 @@ +# mcp-tunnels + +Connect Claude to an MCP server running inside your private network through an +Anthropic **MCP tunnel** — 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 before sending anything sensitive. + +## Commands + +### `/create-docker-mcp-tunnel [deployment-dir]` + +Drives the 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-gateway.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** (`/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 /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.`: + +```bash +scp coder.:/data/ca.crt . +# generic form: scp :~/mcp-tunnel/data/ca.crt . +``` + +Or push from the host to the devbox, if the host can reach it: + +```bash +scp /data/ca.crt @:~/ +``` + +## What gets built + +A small container stack on your host: + +| Container | Role | +|---|---| +| **mcp-gateway** | 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://./` 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, see the official MCP tunnels deployment guides +(Deploy with Docker Compose / Deploy with Helm) in the Claude documentation. + +## Author + +Anthropic (support@anthropic.com) + +## License + +See `LICENSE`. diff --git a/plugins/mcp-tunnels/commands/create-docker-mcp-tunnel.md b/plugins/mcp-tunnels/commands/create-docker-mcp-tunnel.md new file mode 100644 index 00000000..2c02250e --- /dev/null +++ b/plugins/mcp-tunnels/commands/create-docker-mcp-tunnel.md @@ -0,0 +1,356 @@ +--- +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** 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. + +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-gateway** — 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://./` 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): + +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= +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 docs also have 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 < 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-gateway.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: +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 (research-preview +artifacts): + +```yaml +services: + mcp-gateway: + image: us-west1-docker.pkg.dev/proj-mcp-tunnel-poc/mcp-tunnel-poc-console/mcp-proxy@sha256:d00f95322d8f3c0521467f1969fdd89893cd495ffb7dae0c2d6250c7e47b3e26 + volumes: + - ./config/mcp-gateway.yaml:/etc/mcp-gateway/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-gateway" + 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-gateway"` 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 the "Deploy with +Docker Compose" doc; 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-gateway | 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 `.`; 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-gateway"` 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-gateway` — `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 ≠ `*.`, or expired | Regenerate the server cert (Step 4) with the correct `TUNNEL_DOMAIN` | +| `IP validation failed: 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://: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-gateway` (config/cert/routing) are the two primary +diagnostics. Check the outbound connection first, then the inner TLS handshake, +then upstream routing. + +## 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-gateway`. + +## 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 the "Deploy with Docker Compose" / "Deploy +with Helm" guides for a hardened or programmatic-access deployment.