Merge pull request #1 from kacperkwapisz/feat/poke-login-sdk

feat: integrate Poke login SDK token automation into start.sh
This commit is contained in:
0xK
2026-03-24 16:28:55 +01:00
committed by GitHub
3 changed files with 296 additions and 30 deletions
Executable → Regular
+39 -8
View File
@@ -13,18 +13,46 @@ An MCP server that bridges IMAP/SMTP email accounts to [Poke](https://poke.com).
## Quick Start ## Quick Start
Copy this prompt into your AI coding agent (Claude Code, Cursor, etc.): **Prerequisites:** Python 3.10+ and Node.js 18+ (which includes `npx` and `npm`).
```text ```bash
Set up poke-mail (https://github.com/kacperkwapisz/poke-mail) for me — clone the repo, create a Python virtualenv named .venv, install requirements.txt, copy config.example.yml to config.yml, then help me configure config.yml — you can guide me on which IMAP/SMTP host and port to use for my email provider (iCloud, Gmail, Outlook, etc.) and what username format to use, but do NOT type or suggest passwords or API keys, tell me to enter those myself outside of this terminal and confirm when done — then generate a random 32+ character MCP_API_KEY, save it to a .env file as MCP_API_KEY=<the-key>, install the poke npm package globally, run poke login so I can authenticate, and run start.sh to start the server and tunnel it to Poke. git clone https://github.com/kacperkwapisz/poke-mail.git
cd poke-mail
``` ```
To start the server again later: If you haven't logged into Poke yet, do that first — `start.sh` will pick up your token automatically:
```bash
npx poke login
```
Then just run:
```bash ```bash
./start.sh ./start.sh
``` ```
On the **first run**, `start.sh` automatically handles the full setup:
1. Creates a Python virtualenv and installs dependencies
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 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.
### AI coding agent setup
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, 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
### 1. Configure accounts ### 1. Configure accounts
@@ -33,7 +61,7 @@ To start the server again later:
cp config.example.yml config.yml cp config.example.yml config.yml
``` ```
Edit `config.yml` with your email credentials: Edit `config.yml` with your email credentials and Poke API key (from [poke.com/settings/advanced](https://poke.com/settings/advanced)):
```yaml ```yaml
webhook_url: https://poke.com/api/v1/inbound/api-message webhook_url: https://poke.com/api/v1/inbound/api-message
@@ -75,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
@@ -90,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.
@@ -146,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
@@ -601,10 +601,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)
Executable → Regular
+240 -19
View File
@@ -3,41 +3,262 @@ set -euo pipefail
cd "$(dirname "$0")" cd "$(dirname "$0")"
# Check for updates # ── OTA update ────────────────────────────────────────────────────────────────
REPO="kacperkwapisz/poke-mail" # Uses curl + Python stdlib tarfile — no git or unzip required.
LOCAL_SHA=$(git rev-parse HEAD 2>/dev/null || echo "unknown") #
REMOTE_SHA=$(curl -sf "https://api.github.com/repos/${REPO}/commits/main" \ # Flow:
| grep -m1 '"sha"' | cut -d'"' -f4 || echo "") # 1. Fetch latest commit SHA from GitHub API (tiny JSON, ~1 KB, 5 s timeout).
# 2. Compare against .poke_version (last installed SHA). Skip if already
# up to date or if the remote is unreachable.
# 3. Download the repo tarball only when an update exists (30 s timeout).
# 4. Extract with Python tarfile, stripping the GitHub top-level prefix and
# skipping protected local files (.env, config.yml, .venv).
# 5. Persist new SHA to .poke_version and reinstall deps if requirements.txt
# changed.
if [ -n "$REMOTE_SHA" ] && [ "$REMOTE_SHA" != "$LOCAL_SHA" ]; then if command -v curl &>/dev/null && command -v python3 &>/dev/null; then
echo "⚡ A newer version of poke-mail is available." _OTA_REPO="kacperkwapisz/poke-mail"
echo " Local: ${LOCAL_SHA:0:7}" _OTA_BRANCH="main"
echo " Remote: ${REMOTE_SHA:0:7}" _VERSION_FILE=".poke_version"
echo " Run 'git pull' to update."
echo "Checking for updates..."
# Step 1: lightweight SHA check (fail silently if offline)
_REMOTE_SHA=$(curl -sf --max-time 5 \
"https://api.github.com/repos/${_OTA_REPO}/commits/${_OTA_BRANCH}" \
| python3 -c \
"import json,sys; print(json.load(sys.stdin)['sha'])" \
2>/dev/null || echo "")
_LOCAL_SHA=$(cat "$_VERSION_FILE" 2>/dev/null || echo "")
if [ -z "$_REMOTE_SHA" ]; then
echo " Could not reach remote — continuing with local version."
echo ""
elif [ "$_REMOTE_SHA" = "$_LOCAL_SHA" ]; then
echo " ✓ Already up to date (${_REMOTE_SHA:0:7})"
echo ""
else
echo " ↳ Update found (${_LOCAL_SHA:0:7:-}${_REMOTE_SHA:0:7}), downloading..."
# Hash requirements.txt before extraction so we can detect changes
_REQS_BEFORE=$(python3 -c \
"import hashlib; print(hashlib.md5(open('requirements.txt','rb').read()).hexdigest())" \
2>/dev/null || echo "")
_TMP_TAR=$(mktemp /tmp/poke-mail-update.XXXXXX.tar.gz)
if curl -sfL --max-time 30 \
"https://api.github.com/repos/${_OTA_REPO}/tarball/${_OTA_BRANCH}" \
-o "$_TMP_TAR" 2>/dev/null; then
# Extract with Python: strip GitHub's top-level dir, skip protected paths
python3 - "$_TMP_TAR" <<'PYEOF'
import sys, tarfile, os
archive = sys.argv[1]
# Files/dirs that must never be overwritten by an OTA update
PROTECTED = {'.env', 'config.yml', '.venv', '.poke_version'}
try:
with tarfile.open(archive, 'r:gz') as tf:
members = tf.getmembers()
if not members:
sys.exit(0)
# GitHub tarball root dir is e.g. "owner-repo-<sha>/"
prefix = members[0].name.split('/')[0] + '/'
for m in members:
if not m.name.startswith(prefix):
continue
rel = m.name[len(prefix):] # path relative to repo root
if not rel: # skip the root dir entry itself
continue
top = rel.split('/')[0]
if top in PROTECTED:
continue
m.name = rel
try:
tf.extract(m, path='.', set_attrs=False)
except Exception:
pass # best-effort; don't abort on permission issues etc.
except Exception as e:
print(f' ⚠ Extraction error: {e}')
sys.exit(1)
PYEOF
# Persist new SHA so we don't re-download next run
echo "$_REMOTE_SHA" > "$_VERSION_FILE"
echo " ✓ Updated to ${_REMOTE_SHA:0:7}"
# Reinstall deps if requirements.txt changed
_REQS_AFTER=$(python3 -c \
"import hashlib; print(hashlib.md5(open('requirements.txt','rb').read()).hexdigest())" \
2>/dev/null || echo "")
if [ -n "$_REQS_BEFORE" ] && [ "$_REQS_BEFORE" != "$_REQS_AFTER" ]; then
echo " ↳ requirements.txt changed — reinstalling dependencies..."
[ -d .venv ] && source .venv/bin/activate
pip install -q -r requirements.txt
echo " ✓ Dependencies updated"
fi
else
echo " Download failed — continuing with local version."
fi
rm -f "$_TMP_TAR"
echo ""
fi
fi
# ── One-time setup (skipped on subsequent runs) ───────────────────────────────
# 1. Python virtualenv + dependencies
if [ ! -d .venv ]; then
echo "First run — setting up poke-mail..."
echo ""
echo "Creating Python virtualenv (.venv)..."
python3 -m venv .venv
fi
source .venv/bin/activate
if ! python3 -c "import fastmcp" &>/dev/null 2>&1; then
echo "Installing Python dependencies..."
pip install -q -r requirements.txt
echo " ✓ Dependencies installed"
echo "" echo ""
fi fi
# Load .env if present # 2. config.yml — copy example if missing
if [ ! -f config.yml ]; then
echo "Copying config.example.yml → config.yml..."
cp config.example.yml config.yml
echo " ✓ config.yml created"
echo ""
fi
# 3. Poke API key — read from 'poke login' credentials, inject into config.yml
# The 'poke' npm package writes ~/.config/poke/credentials.json { "token": "..." }
# after 'npx poke login'. We auto-read it so the user never has to copy/paste.
if grep -q 'your-api-key-here' config.yml 2>/dev/null; then
POKE_CREDENTIALS_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/poke/credentials.json"
POKE_TOKEN=""
if [ -f "$POKE_CREDENTIALS_FILE" ]; then
POKE_TOKEN=$(python3 -c "
import json, sys
try:
data = json.load(open('$POKE_CREDENTIALS_FILE'))
print(data.get('token', ''))
except Exception:
print('')
" 2>/dev/null || true)
fi
if [ -n "$POKE_TOKEN" ]; then
echo " ✓ Poke API key detected from 'poke login'"
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()
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
echo " ✓ poke_api_key written to config.yml"
echo ""
else
echo " ⚠ poke_api_key not set in config.yml."
echo " Run 'npx poke login' then restart, or paste your key from poke.com/settings/advanced"
echo ""
printf " Poke API key (leave blank to set manually later): "
read -r POKE_TOKEN_INPUT
POKE_TOKEN_INPUT=$(echo "$POKE_TOKEN_INPUT" | tr -d '[:space:]')
if [ -n "$POKE_TOKEN_INPUT" ]; then
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()
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
echo " ✓ poke_api_key saved to config.yml"
fi
echo ""
fi
fi
# 4. MCP_API_KEY — generate once and persist to .env
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
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, 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
else
echo "MCP_API_KEY=${RANDOM_KEY}" > .env
fi
echo " ✓ MCP_API_KEY generated and saved to .env"
echo ""
fi
# ── Load .env ─────────────────────────────────────────────────────────────────
if [ -f .env ]; then if [ -f .env ]; then
set -a set -a
source .env source .env
set +a set +a
fi fi
# Activate virtualenv # ── Tunnel-mode detection ─────────────────────────────────────────────────────
source .venv/bin/activate POKE_TUNNEL="${POKE_TUNNEL:-1}"
: "${MCP_API_KEY:?MCP_API_KEY is not set — add it to .env or export it}" if [ "${POKE_TUNNEL}" != "1" ]; then
: "${MCP_API_KEY:?MCP_API_KEY is not set — add it to .env or export it}"
else
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 poke-mail server in background # ── 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
sleep 2 sleep 2
# Tunnel to Poke
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