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:
0xK
2026-03-24 11:40:26 +01:00
parent 7e94e91753
commit 581dd0b89a
3 changed files with 87 additions and 25 deletions
+13 -5
View File
@@ -13,6 +13,8 @@ An MCP server that bridges IMAP/SMTP email accounts to [Poke](https://poke.com).
## Quick Start ## Quick Start
**Prerequisites:** Python 3.10+ and Node.js 18+ (which includes `npx` and `npm`).
```bash ```bash
git clone https://github.com/kacperkwapisz/poke-mail.git git clone https://github.com/kacperkwapisz/poke-mail.git
cd poke-mail 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` 2. Copies `config.example.yml``config.yml`
3. Reads your Poke API key from `poke login` credentials and injects it into `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` 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. 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.): Copy this prompt into your AI coding agent (Claude Code, Cursor, etc.):
```text ```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 ## Manual Setup
@@ -98,7 +103,7 @@ pip install -r requirements.txt
### 3. Run ### 3. Run
```bash ```bash
MCP_API_KEY=your-secret-key python src/server.py MCP_API_KEY=your-secret-key python3 src/server.py
``` ```
### 4. Test ### 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>`. 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. 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 | | 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 | | `CONFIG_PATH` | `config.yml` | Path to config file |
| `POKE_WEBHOOK_URL` | from config | Overrides webhook URL in config | | `POKE_WEBHOOK_URL` | from config | Overrides webhook URL in config |
| `POKE_API_KEY` | from config | Overrides Poke API key in config | | `POKE_API_KEY` | from config | Overrides Poke API key in config |
Executable → Regular
+17 -3
View File
@@ -492,10 +492,24 @@ async def lifespan(server: FastMCP):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
mcp_api_key = os.environ.get("MCP_API_KEY", "") 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( 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) mcp = FastMCP("poke-mail", lifespan=lifespan, auth=auth)
+57 -17
View File
@@ -52,7 +52,7 @@ if grep -q 'your-api-key-here' config.yml 2>/dev/null; then
if [ -f "$POKE_CREDENTIALS_FILE" ]; then if [ -f "$POKE_CREDENTIALS_FILE" ]; then
POKE_TOKEN=$(python3 -c " POKE_TOKEN=$(python3 -c "
import json import json, sys
try: try:
data = json.load(open('$POKE_CREDENTIALS_FILE')) data = json.load(open('$POKE_CREDENTIALS_FILE'))
print(data.get('token', '')) print(data.get('token', ''))
@@ -63,11 +63,17 @@ except Exception:
if [ -n "$POKE_TOKEN" ]; then if [ -n "$POKE_TOKEN" ]; then
echo " ✓ Poke API key detected from 'poke login'" echo " ✓ Poke API key detected from 'poke login'"
python3 - <<PYEOF # Pass token via env var to avoid shell-interpolation injection in Python source.
import re # 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: with open('config.yml', 'r') as f:
content = f.read() 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: with open('config.yml', 'w') as f:
f.write(new_content) f.write(new_content)
PYEOF PYEOF
@@ -81,11 +87,15 @@ PYEOF
read -r POKE_TOKEN_INPUT read -r POKE_TOKEN_INPUT
POKE_TOKEN_INPUT=$(echo "$POKE_TOKEN_INPUT" | tr -d '[:space:]') POKE_TOKEN_INPUT=$(echo "$POKE_TOKEN_INPUT" | tr -d '[:space:]')
if [ -n "$POKE_TOKEN_INPUT" ]; then if [ -n "$POKE_TOKEN_INPUT" ]; then
python3 - <<PYEOF POKE_TOKEN="$POKE_TOKEN_INPUT" python3 - <<'PYEOF'
import re import os, re, json
token = os.environ['POKE_TOKEN']
with open('config.yml', 'r') as f: with open('config.yml', 'r') as f:
content = f.read() 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: with open('config.yml', 'w') as f:
f.write(new_content) f.write(new_content)
PYEOF PYEOF
@@ -96,20 +106,27 @@ PYEOF
fi fi
# 4. MCP_API_KEY — generate once and persist to .env # 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 " RANDOM_KEY=$(python3 -c "
import secrets, string import secrets, string
alphabet = string.ascii_letters + string.digits alphabet = string.ascii_letters + string.digits
print(''.join(secrets.choice(alphabet) for _ in range(48))) print(''.join(secrets.choice(alphabet) for _ in range(48)))
") ")
if [ -f .env ]; then if [ -f .env ]; then
python3 - <<PYEOF # Anchor to line-start with MULTILINE so only the actual assignment is updated.
import re RANDOM_KEY="$RANDOM_KEY" python3 - <<'PYEOF'
import os, re
new_key = os.environ['RANDOM_KEY']
with open('.env', 'r') as f: with open('.env', 'r') as f:
content = f.read() content = f.read()
new_content = re.sub(r'MCP_API_KEY=.*', 'MCP_API_KEY=${RANDOM_KEY}', content) new_content, n = re.subn(r'(?m)^[[:space:]]*MCP_API_KEY=.*', f'MCP_API_KEY={new_key}', content)
if 'MCP_API_KEY=' not in new_content: if n == 0:
new_content += 'MCP_API_KEY=${RANDOM_KEY}\n' new_content += f'MCP_API_KEY={new_key}\n'
with open('.env', 'w') as f: with open('.env', 'w') as f:
f.write(new_content) f.write(new_content)
PYEOF PYEOF
@@ -127,17 +144,40 @@ if [ -f .env ]; then
set +a set +a
fi fi
: "${MCP_API_KEY:?MCP_API_KEY is not set — add it to .env or export it}" # ── 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 ───────────────────────────────────────────────────── # ── Start server + tunnel ─────────────────────────────────────────────────────
echo "Starting poke-mail server..." echo "Starting poke-mail server..."
python src/server.py & python3 src/server.py &
SERVER_PID=$! SERVER_PID=$!
trap "kill $SERVER_PID 2>/dev/null" EXIT trap "kill $SERVER_PID 2>/dev/null" EXIT
# Wait for server to be ready # Wait for server to be ready
sleep 2 sleep 2
# Tunnel to Poke # Tunnel to Poke — prefer the globally-installed poke binary; fall back to npx.
echo "Starting tunnel to Poke..." echo "Starting tunnel to Poke..."
poke tunnel http://localhost:3000/mcp --name "poke-mail" 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