Connection Modes
This guide shows how to wire the LenserFight MCP server into your product for each supported connection mode. Pick the mode that matches your deployment environment.
| Mode | When to use | Setup time | Requires local repo? |
|---|---|---|---|
| LF Cloud (HTTP) | Production — your product connects to the hosted endpoint | ~5 min | No |
| stdio | Local development inside the LenserFight repo | ~5 min | Yes |
| HTTP + tunnel | Testing local MCP changes before deploying | ~10 min | Yes |
LF Cloud (HTTP)
Use this for all production integrations.
The LenserFight MCP server is proxied through Cloudflare at a stable public domain. There is no separate server to deploy or maintain. Your client sends standard MCP JSON-RPC over HTTPS.
Endpoint
https://mcp.lenserfight.com/mcpOAuth discovery (automatic for compliant clients)
A fully MCP-compliant client reads the discovery document and handles OAuth automatically:
curl https://mcp.lenserfight.com/.well-known/oauth-authorization-server{
"issuer": "https://mcp.lenserfight.com",
"authorization_endpoint": "https://mcp.lenserfight.com/oauth/authorize",
"token_endpoint": "https://mcp.lenserfight.com/oauth/token",
"registration_endpoint": "https://mcp.lenserfight.com/oauth/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"]
}Claude.ai custom connector
- Open claude.ai → Settings → Connectors → Add custom connector.
- Fill in:
- Name:
LenserFight - Remote MCP server URL:
https://mcp.lenserfight.com/mcp - OAuth Client ID: leave blank (dynamic registration)
- OAuth Client Secret: leave blank (PKCE only — no secret)
- Name:
- Click Add.
- Authorize with your LenserFight credentials in the popup.
Cursor
In ~/.cursor/mcp.json (or the per-project .mcp.json in your workspace root):
{
"mcpServers": {
"lenserfight": {
"url": "https://mcp.lenserfight.com/mcp"
}
}
}Restart Cursor. On first use of a LenserFight tool, Cursor opens a browser window for the OAuth flow.
Any HTTP MCP client
Add a remote MCP server entry pointing to the endpoint above. The client must support:
- OAuth 2.1 authorization code flow
- PKCE (S256 code challenge)
- RFC 7591 dynamic client registration (or manually supply a
client_idfrom a prior registration)
If your client does not support dynamic registration, call POST /oauth/register once manually (see OAuth & Authentication) and hard-code the returned client_id.
Custom backend client example (Node.js)
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
const transport = new StreamableHTTPClientTransport(
new URL('https://mcp.lenserfight.com/mcp'),
{
// After completing the OAuth flow, supply the lf_mcp_* access token here
requestInit: {
headers: { Authorization: `Bearer ${process.env.LF_MCP_TOKEN}` },
},
}
)
const client = new Client({ name: 'my-product', version: '1.0.0' }, { capabilities: {} })
await client.connect(transport)
const result = await client.callTool({ name: 'list_lenses', arguments: { limit: 10 } })
console.log(result)stdio (local dev)
Use this only when developing inside the LenserFight repository.
In stdio mode the MCP server is spawned as a child process. Communication happens over stdin/stdout. The service role key bypasses all RLS — this mode is not safe for production use.
Prerequisites
- Local Supabase instance running:
supabase start - MCP server built:
pnpm nx build mcp-server - Node.js 22+, pnpm 9+
Environment variables
| Variable | Value |
|---|---|
SUPABASE_URL | http://127.0.0.1:54321 |
SUPABASE_SERVICE_ROLE_KEY | From supabase status -o env |
SUPABASE_ANON_KEY | From supabase status -o env |
SUPABASE_JWT_SECRET | super-secret-jwt-token-with-at-least-32-characters-long |
MCP_TRANSPORT | stdio |
LENSERFIGHT_LENSER_ID | (optional) UUID of the lenser to scope calls to |
.mcp.json (already in the repo root)
{
"mcpServers": {
"lenserfight": {
"command": "node",
"args": ["dist/apps/mcp-server/main.js"],
"env": {
"SUPABASE_URL": "${SUPABASE_URL}",
"SUPABASE_SERVICE_ROLE_KEY": "${SUPABASE_SERVICE_ROLE_KEY}",
"SUPABASE_ANON_KEY": "${SUPABASE_ANON_KEY}",
"SUPABASE_JWT_SECRET": "${SUPABASE_JWT_SECRET}",
"MCP_TRANSPORT": "stdio"
}
}
}
}Source your env vars, then open Claude Code or Cursor from the repo root. The client reads .mcp.json automatically.
HTTP + tunnel (local dev)
Use this when you want Claude.ai or a remote client to hit your local MCP server changes.
Claude.ai's token exchange happens from Anthropic's cloud. It cannot reach
localhost. You need a public tunnel.
Step 1 — Start ngrok
ngrok http 3001
# Forwarding: https://<id>.ngrok-free.app -> http://localhost:3001The server auto-detects the ngrok public URL from ngrok's local API. No manual copy-paste needed.
With cloudflared instead:
cloudflared tunnel --url http://localhost:3001
export MCP_OAUTH_BASE_URL=https://<your-id>.trycloudflare.comStep 2 — Build and start the server in HTTP mode
Make sure Supabase is running and env vars are sourced, then:
export MCP_TRANSPORT=http
export MCP_HTTP_PORT=3001
node dist/apps/mcp-server/main.jsStartup banner:
[lenserfight-mcp] auto-detected public tunnel: https://<tunnel>.ngrok-free.app
[lenserfight-mcp] HTTP transport ready on http://localhost:3001/mcp
┌─ Local dev OAuth credentials ──────────────────────────────────┐
│ OAuth Client ID : lf_mcp_client_localdev │
│ Auth method : PKCE (no client secret required) │
│ Server base URL : https://<tunnel>.ngrok-free.app │
│ │
│ Claude.ai → Settings → Connectors → Add connector: │
│ URL : https://<tunnel>.ngrok-free.app/mcp │
│ Client ID : lf_mcp_client_localdev │
└─────────────────────────────────────────────────────────────────┘Step 3 — Add the connector in Claude.ai
Use the tunnel URL from the banner, not localhost:
- URL:
https://<tunnel>.ngrok-free.app/mcp - OAuth Client ID:
lf_mcp_client_localdev
After restarting ngrok
The free ngrok tier assigns a new URL on every restart. After each restart:
- Restart the MCP server — it auto-detects the new URL.
- In Claude.ai, delete and re-add the connector with the updated URL.
Troubleshooting
mcp_token_exchange_failed
The discovery document advertised a localhost URL that the remote client cannot reach.
- Confirm the startup banner shows the tunnel URL, not
http://localhost. - Confirm the connector URL in Claude.ai ends with
/mcpand matches the tunnel exactly. - If you restarted ngrok, the URL changed — delete and re-add the connector.
401 Unauthorized on POST /mcp
- The bearer token is missing, expired, or malformed.
- Re-authorize via the connector settings.
- Check that the token format is
lf_mcp_<64 hex chars>.
Sign-in failed
- Wrong email or password in the OAuth login form.
- Confirm credentials at lenserfight.com.
No Lenser profile found
- The user completed account creation but never finished onboarding (picking a handle).
- Send them to lenserfight.com to complete the handle selection step, then retry.
PUBLIC URL REQUIRED warning at startup (tunnel mode)
- ngrok is not running or was not started before the MCP server.
- Start ngrok first, then start the MCP server.
- Or set
MCP_OAUTH_BASE_URL=https://your-urlmanually before starting.
WARN: environment variable is unset
- Forgot to source the env file. Run
source ./.env.mcp.local.