fix: optional MCP_API_KEY in tunnel mode + address Copilot review issues
- start.sh: detect POKE_TUNNEL env var; skip MCP_API_KEY requirement and auth when running via poke tunnel (server.py reads the same var) - start.sh: pass POKE_TOKEN into Python via env var + use json.dumps to safely escape quotes/backslashes in YAML (fixes shell-interpolation injection risk, Copilot issue #5 / start.sh:72) - start.sh: anchor MCP_API_KEY guard to non-commented line-start assignments and also detect empty value (Copilot issues #1, #8 / start.sh:104) - start.sh: anchor re.sub pattern with re.MULTILINE so only the actual assignment line is rewritten, not mid-line occurrences (Copilot issue #2) - start.sh: check re.sub replacement count, warn when poke_api_key key is missing from config.yml (Copilot issue #6) - start.sh: guard npm/npx usage with command -v check; fall back to npx poke instead of hard-failing (Copilot issue #3) - start.sh: use python3 consistently for server.py (Copilot issue #9 / start.sh:134) - start.sh: prefer npx poke tunnel; check command -v poke and fall back gracefully (Copilot issue #10 / start.sh:135) - server.py: honour POKE_TUNNEL=1 — skip bearer-token auth so the poke tunnel handles identity; MCP_API_KEY becomes optional in that mode - README.md: add Node.js/npm prerequisite note (Copilot issue #4) - README.md: clarify server starts on first run; update AI agent prompt (Copilot issue #11 / README.md:48)
This commit is contained in:
@@ -13,6 +13,8 @@ An MCP server that bridges IMAP/SMTP email accounts to [Poke](https://poke.com).
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Prerequisites:** Python 3.10+ and Node.js 18+ (which includes `npx` and `npm`).
|
||||
|
||||
```bash
|
||||
git clone https://github.com/kacperkwapisz/poke-mail.git
|
||||
cd poke-mail
|
||||
@@ -35,8 +37,11 @@ On the **first run**, `start.sh` automatically handles the full setup:
|
||||
2. Copies `config.example.yml` → `config.yml`
|
||||
3. Reads your Poke API key from `poke login` credentials and injects it into `config.yml`
|
||||
4. Generates a random `MCP_API_KEY` and saves it to `.env`
|
||||
5. Immediately starts the server and tunnel
|
||||
|
||||
After that first run, open `config.yml` and fill in your email account credentials. Run `./start.sh` again to start the server.
|
||||
After the first run completes (or if the server exits with email auth errors), open `config.yml` and fill in your email account credentials, then run `./start.sh` again.
|
||||
|
||||
> **Note:** If your email credentials in `config.yml` are still placeholders, IMAP/SMTP connections will fail on startup. Update the file and rerun `./start.sh`.
|
||||
|
||||
On **subsequent runs**, `start.sh` skips setup and goes straight to starting the server and tunnel.
|
||||
|
||||
@@ -45,7 +50,7 @@ On **subsequent runs**, `start.sh` skips setup and goes straight to starting the
|
||||
Copy this prompt into your AI coding agent (Claude Code, Cursor, etc.):
|
||||
|
||||
```text
|
||||
Set up poke-mail (https://github.com/kacperkwapisz/poke-mail) for me — clone the repo, run 'npx poke login' so I can authenticate with Poke (wait for me to confirm), then run './start.sh' which will automatically wire up my Poke API key, generate an MCP_API_KEY, and set up the virtualenv — then help me fill in my email credentials in config.yml (guide me on IMAP/SMTP host and port for my provider but do NOT type passwords or secrets — tell me to enter those myself and confirm when done) — then run ./start.sh again to start the server and tunnel it to Poke.
|
||||
Set up poke-mail (https://github.com/kacperkwapisz/poke-mail) for me — clone the repo, run 'npx poke login' so I can authenticate with Poke (wait for me to confirm), then run './start.sh' which will automatically wire up my Poke API key, generate an MCP_API_KEY, set up the virtualenv, and start the server and tunnel — then help me fill in my email credentials in config.yml (guide me on IMAP/SMTP host and port for my provider but do NOT type passwords or secrets — tell me to enter those myself and confirm when done); if the server fails due to missing/invalid email credentials, have me update config.yml and run './start.sh' again to restart it.
|
||||
```
|
||||
|
||||
## Manual Setup
|
||||
@@ -98,7 +103,7 @@ pip install -r requirements.txt
|
||||
### 3. Run
|
||||
|
||||
```bash
|
||||
MCP_API_KEY=your-secret-key python src/server.py
|
||||
MCP_API_KEY=your-secret-key python3 src/server.py
|
||||
```
|
||||
|
||||
### 4. Test
|
||||
@@ -113,7 +118,9 @@ Open http://localhost:3000 and connect to `http://localhost:3000/mcp` using "Str
|
||||
|
||||
Set `MCP_API_KEY` to secure the server. All requests must include `Authorization: Bearer <MCP_API_KEY>`.
|
||||
|
||||
If `MCP_API_KEY` is not set, the server runs unauthenticated (with a warning). **Always set it in production.**
|
||||
When running via `start.sh` (which uses `poke tunnel`), set `POKE_TUNNEL=1` to make `MCP_API_KEY` optional — the tunnel handles authentication. `start.sh` sets this automatically.
|
||||
|
||||
If `MCP_API_KEY` is not set and `POKE_TUNNEL` is not `1`, the server runs unauthenticated (with a warning). **Always set it in non-tunnel deployments.**
|
||||
|
||||
When connecting from Poke, add the bearer token in your connection settings.
|
||||
|
||||
@@ -169,7 +176,8 @@ The server is mostly idle (IMAP IDLE + lightweight HTTP). Recommended limits for
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `MCP_API_KEY` | — | **Required in production.** Bearer token to secure the MCP server |
|
||||
| `MCP_API_KEY` | — | Bearer token to secure the MCP server. Optional when `POKE_TUNNEL=1`. |
|
||||
| `POKE_TUNNEL` | `0` | Set to `1` when running behind the poke tunnel — skips `MCP_API_KEY` auth requirement. `start.sh` sets this automatically. |
|
||||
| `CONFIG_PATH` | `config.yml` | Path to config file |
|
||||
| `POKE_WEBHOOK_URL` | from config | Overrides webhook URL in config |
|
||||
| `POKE_API_KEY` | from config | Overrides Poke API key in config |
|
||||
|
||||
Executable → Regular
+17
-3
@@ -492,10 +492,24 @@ async def lifespan(server: FastMCP):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
mcp_api_key = os.environ.get("MCP_API_KEY", "")
|
||||
auth = ApiKeyAuth(mcp_api_key) if mcp_api_key else None
|
||||
if not mcp_api_key:
|
||||
|
||||
# When running behind the poke tunnel (POKE_TUNNEL=1), the tunnel handles
|
||||
# authentication so the MCP_API_KEY bearer check is optional.
|
||||
# In direct / Docker deployments the key is still required for security.
|
||||
poke_tunnel_mode = os.environ.get("POKE_TUNNEL", "") == "1"
|
||||
|
||||
if mcp_api_key:
|
||||
auth = ApiKeyAuth(mcp_api_key)
|
||||
elif poke_tunnel_mode:
|
||||
auth = None # tunnel handles auth
|
||||
logger.info(
|
||||
"POKE_TUNNEL=1 detected — MCP_API_KEY not required (tunnel handles auth)."
|
||||
)
|
||||
else:
|
||||
auth = None
|
||||
logger.warning(
|
||||
"MCP_API_KEY not set — server is unauthenticated. Set MCP_API_KEY to secure it."
|
||||
"MCP_API_KEY not set — server is unauthenticated. "
|
||||
"Set MCP_API_KEY or use POKE_TUNNEL=1 to silence this warning."
|
||||
)
|
||||
|
||||
mcp = FastMCP("poke-mail", lifespan=lifespan, auth=auth)
|
||||
|
||||
@@ -52,7 +52,7 @@ if grep -q 'your-api-key-here' config.yml 2>/dev/null; then
|
||||
|
||||
if [ -f "$POKE_CREDENTIALS_FILE" ]; then
|
||||
POKE_TOKEN=$(python3 -c "
|
||||
import json
|
||||
import json, sys
|
||||
try:
|
||||
data = json.load(open('$POKE_CREDENTIALS_FILE'))
|
||||
print(data.get('token', ''))
|
||||
@@ -63,11 +63,17 @@ except Exception:
|
||||
|
||||
if [ -n "$POKE_TOKEN" ]; then
|
||||
echo " ✓ Poke API key detected from 'poke login'"
|
||||
python3 - <<PYEOF
|
||||
import re
|
||||
# Pass token via env var to avoid shell-interpolation injection in Python source.
|
||||
# json.dumps handles quoting/escaping so the result is valid YAML.
|
||||
POKE_TOKEN="$POKE_TOKEN" python3 - <<'PYEOF'
|
||||
import os, re, json
|
||||
token = os.environ['POKE_TOKEN']
|
||||
with open('config.yml', 'r') as f:
|
||||
content = f.read()
|
||||
new_content = re.sub(r'(poke_api_key:\s*)[^\n]+', r'\g<1>"${POKE_TOKEN}"', content)
|
||||
pattern = r'(?m)^([ \t]*poke_api_key:[ \t*])[^\n]+'
|
||||
new_content, n = re.subn(pattern, lambda m: m.group(1) + json.dumps(token), content)
|
||||
if n == 0:
|
||||
print(' ⚠ Warning: poke_api_key key not found in config.yml — update it manually.')
|
||||
with open('config.yml', 'w') as f:
|
||||
f.write(new_content)
|
||||
PYEOF
|
||||
@@ -81,11 +87,15 @@ PYEOF
|
||||
read -r POKE_TOKEN_INPUT
|
||||
POKE_TOKEN_INPUT=$(echo "$POKE_TOKEN_INPUT" | tr -d '[:space:]')
|
||||
if [ -n "$POKE_TOKEN_INPUT" ]; then
|
||||
python3 - <<PYEOF
|
||||
import re
|
||||
POKE_TOKEN="$POKE_TOKEN_INPUT" python3 - <<'PYEOF'
|
||||
import os, re, json
|
||||
token = os.environ['POKE_TOKEN']
|
||||
with open('config.yml', 'r') as f:
|
||||
content = f.read()
|
||||
new_content = re.sub(r'(poke_api_key:\s*)[^\n]+', r'\g<1>"${POKE_TOKEN_INPUT}"', content)
|
||||
pattern = r'(?m)^([ \t]*poke_api_key:[ \t*])[^\n]+'
|
||||
new_content, n = re.subn(pattern, lambda m: m.group(1) + json.dumps(token), content)
|
||||
if n == 0:
|
||||
print(' ⚠ Warning: poke_api_key key not found in config.yml — update it manually.')
|
||||
with open('config.yml', 'w') as f:
|
||||
f.write(new_content)
|
||||
PYEOF
|
||||
@@ -96,20 +106,27 @@ PYEOF
|
||||
fi
|
||||
|
||||
# 4. MCP_API_KEY — generate once and persist to .env
|
||||
if [ ! -f .env ] || grep -q 'your-secret-key-here' .env 2>/dev/null || ! grep -q 'MCP_API_KEY=' .env 2>/dev/null; then
|
||||
# Regenerate when: .env is missing, contains the placeholder, or has an
|
||||
# empty assignment (MCP_API_KEY=) which would still fail at the :? check.
|
||||
# Anchored to non-commented, line-start assignments only.
|
||||
if [ ! -f .env ] \
|
||||
|| grep -Eq '^[[:space:]]*MCP_API_KEY=your-secret-key-here' .env 2>/dev/null \
|
||||
|| ! grep -Eq '^[[:space:]]*MCP_API_KEY=.+' .env 2>/dev/null; then
|
||||
RANDOM_KEY=$(python3 -c "
|
||||
import secrets, string
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
print(''.join(secrets.choice(alphabet) for _ in range(48)))
|
||||
")
|
||||
if [ -f .env ]; then
|
||||
python3 - <<PYEOF
|
||||
import re
|
||||
# Anchor to line-start with MULTILINE so only the actual assignment is updated.
|
||||
RANDOM_KEY="$RANDOM_KEY" python3 - <<'PYEOF'
|
||||
import os, re
|
||||
new_key = os.environ['RANDOM_KEY']
|
||||
with open('.env', 'r') as f:
|
||||
content = f.read()
|
||||
new_content = re.sub(r'MCP_API_KEY=.*', 'MCP_API_KEY=${RANDOM_KEY}', content)
|
||||
if 'MCP_API_KEY=' not in new_content:
|
||||
new_content += 'MCP_API_KEY=${RANDOM_KEY}\n'
|
||||
new_content, n = re.subn(r'(?m)^[[:space:]]*MCP_API_KEY=.*', f'MCP_API_KEY={new_key}', content)
|
||||
if n == 0:
|
||||
new_content += f'MCP_API_KEY={new_key}\n'
|
||||
with open('.env', 'w') as f:
|
||||
f.write(new_content)
|
||||
PYEOF
|
||||
@@ -127,17 +144,40 @@ if [ -f .env ]; then
|
||||
set +a
|
||||
fi
|
||||
|
||||
# ── Tunnel-mode detection ─────────────────────────────────────────────────────
|
||||
# When POKE_TUNNEL=1 the poke tunnel handles auth — MCP_API_KEY is optional.
|
||||
# In all other modes (direct HTTP, Docker, etc.) it is required.
|
||||
POKE_TUNNEL="${POKE_TUNNEL:-1}" # default to tunnel mode since start.sh always tunnels
|
||||
|
||||
if [ "${POKE_TUNNEL}" != "1" ]; then
|
||||
: "${MCP_API_KEY:?MCP_API_KEY is not set — add it to .env or export it}"
|
||||
else
|
||||
# In tunnel mode warn when the key is absent but don't abort.
|
||||
if [ -z "${MCP_API_KEY:-}" ]; then
|
||||
echo " ℹ MCP_API_KEY not set — server runs unauthenticated (safe: poke tunnel handles auth)."
|
||||
fi
|
||||
export POKE_TUNNEL
|
||||
fi
|
||||
|
||||
# ── Start server + tunnel ─────────────────────────────────────────────────────
|
||||
echo "Starting poke-mail server..."
|
||||
python src/server.py &
|
||||
python3 src/server.py &
|
||||
SERVER_PID=$!
|
||||
trap "kill $SERVER_PID 2>/dev/null" EXIT
|
||||
|
||||
# Wait for server to be ready
|
||||
sleep 2
|
||||
|
||||
# Tunnel to Poke
|
||||
# Tunnel to Poke — prefer the globally-installed poke binary; fall back to npx.
|
||||
echo "Starting tunnel to Poke..."
|
||||
if command -v poke &>/dev/null; then
|
||||
poke tunnel http://localhost:3000/mcp --name "poke-mail"
|
||||
else
|
||||
echo " ℹ 'poke' binary not found in PATH — using npx poke (requires Node.js)."
|
||||
if ! command -v npx &>/dev/null; then
|
||||
echo " ✗ Neither 'poke' nor 'npx' found. Install Node.js (nodejs.org) and run:"
|
||||
echo " npm install -g poke OR npx poke tunnel ..."
|
||||
exit 1
|
||||
fi
|
||||
npx --yes poke tunnel http://localhost:3000/mcp --name "poke-mail"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user