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

Add mcp-tunnels plugin
This commit is contained in:
Den Delimarsky 2026-05-20 23:16:43 -07:00 committed by GitHub
commit 6cc16f4b16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 712 additions and 0 deletions

View File

@ -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.",

View File

@ -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
plugins/mcp-tunnels/LICENSE Normal file
View File

@ -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.

View File

@ -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`.

View File

@ -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.