Skip to main content
Glama

local-mcp-gateway

CLI: lmg

Expose localhost MCP servers (Paper Desktop, etc.) through a small authenticated proxy and a pluggable tunnel (Tailscale Funnel, Cloudflare Tunnel, or ngrok) so Cursor Cloud Agents and other remote clients can reach them.

Why this exists

Paper Desktop only listens on loopback. Cloud Agents cannot reach 127.0.0.1 on your laptop.

Two constraints that bite in practice:

  1. Host rewrite. Paper rejects MCP requests unless the HTTP Host header is 127.0.0.1:29979 (DNS-rebinding protection). Tailscale / Cloudflare / ngrok send their public hostname → Paper returns 403 Invalid host. The gateway rewrites Host and strips Authorization, Cookie, and Origin before proxying upstream.

  2. Public HTTPS for Cloud HTTP MCP. Cursor Cloud Agent HTTP MCP is proxied from Cursor’s backend, which is not on your tailnet. A tailnet-only URL (tailscale serve without Funnel) is unreachable. Use Funnel, Cloudflare, or ngrok for Cloud Agents. Tailnet-only serve is still valid for other tailnet clients.

Related MCP server: cursor-agent-bridge

Install

# from this repo
uv sync
uv run lmg --help

# or as a tool
uv tool install .
# later: uv tool install git+https://github.com/ppcantidio/local-mcp-gateway

Requires Python 3.12+.

Upgrade (force)

uv tool install does not restart a running lmg serve. Reinstall, then kill and start again:

cd ~/Developer/pessoal/local-mcp-gateway   # your clone
git pull

# wipe the installed tool and reinstall from this tree
uv tool uninstall local-mcp-gateway || true
uv tool install --force --reinstall .

lmg --version    # expect 0.1.4+
# Ctrl+C the old serve (or: pkill -f 'lmg serve')
lmg serve --publisher tailscale --mode funnel

curl -s http://127.0.0.1:8788/healthz
# expect version 0.1.4, sse_unwrap true, get_sse_disabled false

If lmg --version is still old, check which lmg — another install (venv / old path) may be first on PATH.

Quick start

lmg init                          # writes ./lmg.toml, prints LMG_API_KEY once
export LMG_API_KEY=...            # paste the key from init / keygen

# Paper Desktop open with a file loaded (confirms http://127.0.0.1:29979/mcp)
lmg serve --publisher tailscale --mode funnel

Register more local MCPs (file or CLI):

lmg add paper http://127.0.0.1:29979 --rewrite-host 127.0.0.1:29979
lmg add other http://127.0.0.1:3100
lmg ls
lmg rm other

# ephemeral for one run
lmg serve --publisher local --mcp demo=http://127.0.0.1:3200

Config search order: --config./lmg.toml~/.config/lmg/config.toml.

LMG_API_KEY is required for serve. It is never written to the TOML file and never logged.

Routing

Each registered MCP is exposed under its name:

Local upstream

Public URL

paperhttp://127.0.0.1:29979/mcp

https://<published-host>/paper/mcp

GET / and GET /healthz are unauthenticated (tunnel probes). Everything else requires Authorization: Bearer <LMG_API_KEY>.

Reliability (Cloud Agents + Funnel)

Cursor Cloud MCP discovery opens a GET SSE stream. Returning 405 makes the namespace look "ready" with 0 tools, so GET SSE stays enabled by default. Funnel idle drops are mitigated with SSE heartbeats.

This gateway now:

  1. Keeps GET SSE enabled by default (set proxy.disable_get_sse = true only for POST-only experiments).

  2. Injects SSE keepalives on idle GET streams (proxy.sse_heartbeat_seconds, default 15).

  3. Retries upstream Paper/Desktop connection blips (proxy.upstream_retries, default 2).

  4. Gzip-compresses larger JSON responses.

[proxy]
# disable_get_sse = false
sse_heartbeat_seconds = 15
upstream_retries = 2

GET /healthz reports get_sse_disabled and upstream_retries so you can confirm the live process.

Prefer get_basic_info → artboard/nodeIdget_tree_summary with a low depth for huge Paper files.

Publishers

Name

Mode

Public?

Notes

local

No

http://127.0.0.1:<port> — tests / laptop only

tailscale

serve

Tailnet only

Not enough for Cursor Cloud HTTP MCP

tailscale

funnel

Yes

Public HTTPS; needs Funnel enabled on the tailnet

cloudflare

Yes

cloudflared tunnel --url … (quick tunnel)

ngrok

Yes

ngrok http <port>

The proxy only binds localhost. A Publisher starts after the server is listening and returns the public origin. Adding a publisher = one module + registry entry (no if publisher == in the proxy).

CLIs must already be on PATH. Missing binary errors include an install hint.

Tailscale stop: lmg turns off the --https=443 serve/funnel mapping it created. It does not wipe unrelated Tailscale serve routes; if you share port 443 with other mappings, stop carefully.

Cursor plugin (Cloud Agents)

This repo is a Team Marketplace. Import it so Cloud Agents can use Paper over your public gateway.

  1. Dashboard → PluginsTeam MarketplacesAdd Marketplace → import
    https://github.com/ppcantidio/local-mcp-gateway

  2. Install the paper-cloud-mcp plugin (not the local Paper Desktop MCP).

  3. Configure variables (same fixed values as your laptop):

Variable

Example

LMG_PAPER_MCP_URL

https://laptop-de-pedro.tail0ee25d.ts.net/paper/mcp

LMG_API_KEY

value from ~/.config/lmg/env

  1. Enable the MCP for Cloud Agents at cursor.com/agents.

  2. Keep lmg serve + Paper Desktop running on the Mac while agents work.

Plugin sources live under plugins/paper-cloud-mcp/ (manifest + mcp.json). Never commit API keys.

Security

Funnel / Cloudflare / ngrok put a public HTTPS endpoint on the internet. The API key is the only gate.

  • Generate a long token_urlsafe key (lmg keygen)

  • Rotate when shared or leaked

  • Never commit LMG_API_KEY or a TOML file that contains secrets

  • Turn Funnel / tunnels off when idle

CLI

Command

Purpose

lmg init

Write lmg.toml from the example; print a key once

lmg keygen

Print a new key (not written to disk)

lmg add NAME URL

Register a local MCP

lmg ls / lmg rm NAME

List / remove

lmg serve

Start proxy + publisher

Develop

uv sync
uv run pytest
uv run ruff check
uv run ty check

Package layout

src/local_mcp_gateway/
  cli/          # Typer entrypoint (`lmg`) — thin UX layer
  config/       # TOML models, file IO, env secrets (LMG_API_KEY)
  proxy/        # Starlette app: auth, routing, Host rewrite, SSE
  publishers/   # Pluggable tunnels (local / tailscale / cloudflare / ngrok)
  runtime/      # Process runner + proxy/publisher lifecycle
  data/         # Packaged example TOML
  errors.py     # Shared exceptions

Out of scope (v1)

OAuth / Tailscale identity headers, Windows-specific installers, putting this in the Spryx monorepo, committing API keys.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables MCP clients to connect to a local workspace over a public tunnel and lets them run shell commands and transfer files bidirectionally.
    262 npm
    21
    GPL 3.0