Metadata-Version: 2.5
Name: funnel-mcp-bridge
Version: 0.1.0
Summary: Bridge the hosted Funnel MCP endpoint onto stdio for MCP Anywhere
License: MIT
Requires-Python: >=3.11
Requires-Dist: fastmcp>=2.9
Requires-Dist: httpx2>=2.0
Requires-Dist: starlette>=0.37
Requires-Dist: uvicorn>=0.30
Description-Content-Type: text/markdown

# funnel-mcp-bridge

Makes the hosted Funnel MCP endpoint usable from MCP Anywhere.

MCP Anywhere runs servers as local processes and talks to them over stdio. Funnel is a remote HTTPS endpoint behind OAuth, with no code to install. This package sits between the two: stdio on one side, authenticated HTTP to `https://mcp.ai.funnel.io/mcp` on the other.

## Why the broker exists

Funnel enforces strict refresh token rotation. Every exchange invalidates the previous refresh token and issues a replacement, which must be saved immediately. MCP Anywhere containers are rebuilt on startup and have no writable storage, so a token placed in configuration works exactly once.

So the token lives in one long running process, the broker, which is the only thing that ever refreshes. Containers hold nothing and ask for a short lived access token.

```
MCP Anywhere container            your infrastructure          Funnel
+------------------------+        +-----------------+
| funnel-mcp-bridge serve| -----> | broker          | -------> login.funnel.io
|  (stdio <-> HTTP)      | token  |  owns refresh   |  refresh
+------------------------+        |  token on disk  |
            |                     +-----------------+
            +-----------------------------------------------> mcp.ai.funnel.io/mcp
                                            bearer access token
```

If Funnel ever relaxes rotation, or issues you a confidential client where rotation is not enforced, drop the broker and set `FUNNEL_REFRESH_TOKEN` directly on the server in MCP Anywhere. The bridge supports both.

## Setup

### 1. Mint the refresh token

Once, on a machine with a browser, signed in as the Funnel account that should back the shared connection. Use a dedicated account, not a person's, because everyone behind the gateway will query as this identity.

```bash
uvx --from funnel-mcp-bridge funnel-mcp-bridge login
```

This runs a PKCE flow against `login.funnel.io` with a localhost callback, requests `offline_access`, and saves the refresh token to `~/.funnel-mcp-bridge/tokens.json`.

Do not use `mcp-remote` to mint this token. It performs its own refresh, which rotates the token and leaves any copy you extracted dead.

### 2. Run the broker

On infrastructure with persistent storage. Run exactly one instance: two instances sharing a store will fight over the chain and one is locked out permanently.

```bash
export FUNNEL_TOKEN_STORE=/var/lib/funnel-mcp/tokens.json
export FUNNEL_BROKER_API_KEY="$(openssl rand -hex 32)"
uvx --from funnel-mcp-bridge funnel-mcp-bridge broker --host 0.0.0.0 --port 8080
```

Alert on `GET /healthz`. A 503 with `reauthorization_required` means the chain is broken and only a fresh `login` recovers it.

### 3. Add it in MCP Anywhere

Add server, manual entry, pointing at your fork of this repo. Runtime is `uvx` because this is a Python package; `npx` is for npm packages only.

Neither MCP Anywhere base image has `git`, so the install command must not resolve through git. Install from PyPI by name, or from a GitHub release tarball over HTTPS. Use module invocation for the start command, because `uv tool install` puts entry points in `~/.local/bin`, which is not on `PATH` in `python:3.11-slim`.

| Field | Value |
| --- | --- |
| Runtime type | `uvx` |
| GitHub URL | `https://github.com/locomotive-agency/funnel-mcp-bridge` |
| Install command | `pip install funnel-mcp-bridge` |
| Start command | `python -m funnel_mcp_bridge serve` |

Environment variables on the server:

| Variable | Value |
| --- | --- |
| `FUNNEL_BROKER_URL` | `https://your-broker.internal:8080` |
| `FUNNEL_BROKER_API_KEY` | the key from step 2 |

## Configuration

| Variable | Default | Notes |
| --- | --- | --- |
| `FUNNEL_MCP_URL` | `https://mcp.ai.funnel.io/mcp` | Use the EU endpoint for EU residency workspaces. Also the token audience. |
| `FUNNEL_OAUTH_ISSUER` | `https://login.funnel.io` | |
| `FUNNEL_CLIENT_ID` | Funnel's published client ID | Override if Funnel registers a client for you. |
| `FUNNEL_CLIENT_SECRET` | unset | Set for a confidential client. |
| `FUNNEL_BROKER_URL` | unset | Broker mode. Takes precedence. |
| `FUNNEL_BROKER_API_KEY` | unset | Shared between broker and bridge. |
| `FUNNEL_REFRESH_TOKEN` | unset | Static mode. Only safe without rotation. |
| `FUNNEL_TOKEN_STORE` | `~/.funnel-mcp-bridge/tokens.json` | Broker only. |
| `FUNNEL_REFRESH_MARGIN` | `900` | Seconds before expiry to refresh. |
| `FUNNEL_LOG_LEVEL` | `INFO` | Logs go to stderr; stdout carries the protocol. |

## Known constraints

Everyone behind the gateway shares one Funnel identity, so Funnel's audit trail shows a single account and access is whatever that account can see. Scope the account to the workspaces that should be shared.

Whether the refresh chain has an absolute lifetime is unconfirmed. If it does, the `login` step recurs on that schedule regardless of use.

## Tests

```bash
pytest
```

The suite runs a fake authorization server and a fake Funnel MCP endpoint, and covers bearer injection, 401 recovery, rotation survival through the store, and the failure that makes static mode unsafe.