From 45f75d9e1211ed60d9ce1ecee866ade1f38c7890 Mon Sep 17 00:00:00 2001 From: 0xK Date: Tue, 24 Mar 2026 09:43:44 +0100 Subject: [PATCH 1/8] feat: add setup.sh with Poke login SDK token automation --- README.md | 38 +++++++++++++- setup.sh | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 2 deletions(-) mode change 100755 => 100644 README.md create mode 100644 setup.sh diff --git a/README.md b/README.md old mode 100755 new mode 100644 index 023bd48..73838db --- a/README.md +++ b/README.md @@ -13,10 +13,44 @@ An MCP server that bridges IMAP/SMTP email accounts to [Poke](https://poke.com). ## Quick Start +### Automated setup (recommended) + +```bash +git clone https://github.com/kacperkwapisz/poke-mail.git +cd poke-mail +``` + +If you haven't logged into Poke yet, do that first — it lets `setup.sh` pick up your token automatically: + +```bash +npx poke login +``` + +Then run the setup script: + +```bash +bash setup.sh +``` + +`setup.sh` will: +1. Create a Python virtualenv and install dependencies +2. Detect your Poke API key from `poke login` credentials (or prompt you to paste one) +3. Copy `config.example.yml` → `config.yml` and inject the API key automatically +4. Generate a random `MCP_API_KEY` and write it to `.env` +5. Ensure the `poke` npm package is installed globally + +After setup, open `config.yml` and fill in your email account credentials, then start the server: + +```bash +./start.sh +``` + +### 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, 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=, 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. +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 'bash setup.sh' which will automatically wire up my Poke API key, generate an MCP_API_KEY, and set up the virtualenv — then help me configure config.yml with my email credentials (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 to start the server and tunnel it to Poke. ``` To start the server again later: @@ -33,7 +67,7 @@ To start the server again later: 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 webhook_url: https://poke.com/api/v1/inbound/api-message diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..7a2a09e --- /dev/null +++ b/setup.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# setup.sh — one-time setup for poke-mail +# Automates token creation via the Poke login SDK so you don't have to +# copy/paste API keys manually. +set -euo pipefail + +cd "$(dirname "$0")" + +echo "" +echo " poke-mail setup" +echo " ────────────────────────────────────────" +echo "" + +# ── 1. Python virtualenv ──────────────────────────────────────────────────── +if [ ! -d .venv ]; then + echo "Creating Python virtualenv (.venv)..." + python3 -m venv .venv +fi +source .venv/bin/activate +echo "Installing Python dependencies..." +pip install -q -r requirements.txt +echo " ✓ Python dependencies installed" +echo "" + +# ── 2. Poke API key (poke_api_key for config.yml) ──────────────────────────── +# The 'poke' npm package stores credentials at: +# ~/.config/poke/credentials.json → { "token": "..." } +# when the user runs: npx poke login + +POKE_CREDENTIALS_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/poke/credentials.json" +POKE_API_KEY_VALUE="" + +if [ -f "$POKE_CREDENTIALS_FILE" ]; then + # Try to extract .token with python (avoids jq dependency) + POKE_API_KEY_VALUE=$(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_API_KEY_VALUE" ]; then + echo " ✓ Found Poke credentials from 'poke login'" +else + echo " Poke API key not found via 'poke login'." + echo "" + echo " Option 1 (recommended): Run the following, then re-run this script:" + echo " npx poke login" + echo "" + echo " Option 2: Paste your API key from https://poke.com/settings/advanced" + echo "" + printf " Poke API key (leave blank to set later): " + read -r POKE_API_KEY_VALUE + POKE_API_KEY_VALUE=$(echo "$POKE_API_KEY_VALUE" | tr -d '[:space:]') +fi +echo "" + +# ── 3. config.yml ──────────────────────────────────────────────────────────── +if [ ! -f config.yml ]; then + echo "Copying config.example.yml → config.yml..." + cp config.example.yml config.yml + echo " ✓ config.yml created" +else + echo " ✓ config.yml already exists — skipping copy" +fi + +# Inject poke_api_key into config.yml if we have one +if [ -n "$POKE_API_KEY_VALUE" ]; then + # Replace the placeholder value in-place (handles both quoted forms) + python3 - <"${POKE_API_KEY_VALUE}"' +new_content = re.sub(pattern, replacement, content) + +with open('config.yml', 'w') as f: + f.write(new_content) + +print(' ✓ poke_api_key written to config.yml') +EOF +else + echo " ⚠ poke_api_key not set — edit config.yml manually before running start.sh" +fi +echo "" + +# ── 4. MCP_API_KEY (.env) ──────────────────────────────────────────────────── +if [ -f .env ] && grep -q 'MCP_API_KEY=' .env && ! grep -q 'MCP_API_KEY=your-secret-key-here' .env; then + echo " ✓ .env already has MCP_API_KEY — skipping" +else + echo "Generating MCP_API_KEY..." + 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 + # Update existing file + python3 - < .env + fi + echo " ✓ MCP_API_KEY generated and saved to .env" +fi +echo "" + +# ── 5. Poke npm package ─────────────────────────────────────────────────────── +if ! command -v poke &>/dev/null; then + echo "Installing the 'poke' npm package globally..." + npm install -g poke + echo " ✓ poke installed" +else + echo " ✓ poke CLI already installed ($(poke --version 2>/dev/null || echo 'version unknown'))" +fi +echo "" + +# ── Done ───────────────────────────────────────────────────────────────────── +echo " ────────────────────────────────────────" +echo " Setup complete!" +echo "" +if [ -z "$POKE_API_KEY_VALUE" ]; then + echo " Next steps:" + echo " 1. Run 'npx poke login' or add your Poke API key to config.yml" + echo " 2. Fill in your email credentials in config.yml" + echo " 3. Run ./start.sh" +else + echo " Next steps:" + echo " 1. Fill in your email credentials in config.yml" + echo " 2. Run ./start.sh" +fi +echo "" From 37f73e3a9f629e02f49a22808071da94643d6114 Mon Sep 17 00:00:00 2001 From: 0xK Date: Tue, 24 Mar 2026 10:02:44 +0100 Subject: [PATCH 2/8] refactor: merge setup logic into start.sh, remove setup.sh --- README.md | 37 +++++++----------- start.sh | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 119 insertions(+), 30 deletions(-) mode change 100755 => 100644 start.sh diff --git a/README.md b/README.md index 73838db..91491e8 100644 --- a/README.md +++ b/README.md @@ -13,50 +13,39 @@ An MCP server that bridges IMAP/SMTP email accounts to [Poke](https://poke.com). ## Quick Start -### Automated setup (recommended) - ```bash git clone https://github.com/kacperkwapisz/poke-mail.git cd poke-mail ``` -If you haven't logged into Poke yet, do that first — it lets `setup.sh` pick up your token automatically: +If you haven't logged into Poke yet, do that first — `start.sh` will pick up your token automatically: ```bash npx poke login ``` -Then run the setup script: - -```bash -bash setup.sh -``` - -`setup.sh` will: -1. Create a Python virtualenv and install dependencies -2. Detect your Poke API key from `poke login` credentials (or prompt you to paste one) -3. Copy `config.example.yml` → `config.yml` and inject the API key automatically -4. Generate a random `MCP_API_KEY` and write it to `.env` -5. Ensure the `poke` npm package is installed globally - -After setup, open `config.yml` and fill in your email account credentials, then start the server: +Then just run: ```bash ./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` + +After that first run, open `config.yml` and fill in your email account credentials. Run `./start.sh` again to start the server. + +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 'bash setup.sh' which will automatically wire up my Poke API key, generate an MCP_API_KEY, and set up the virtualenv — then help me configure config.yml with my email credentials (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 to start the server and tunnel it to Poke. -``` - -To start the server again later: - -```bash -./start.sh +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. ``` ## Manual Setup diff --git a/start.sh b/start.sh old mode 100755 new mode 100644 index b336914..7032b37 --- a/start.sh +++ b/start.sh @@ -3,7 +3,7 @@ set -euo pipefail cd "$(dirname "$0")" -# Check for updates +# ── Check for updates ───────────────────────────────────────────────────────── REPO="kacperkwapisz/poke-mail" LOCAL_SHA=$(git rev-parse HEAD 2>/dev/null || echo "unknown") REMOTE_SHA=$(curl -sf "https://api.github.com/repos/${REPO}/commits/main" \ @@ -17,19 +17,119 @@ if [ -n "$REMOTE_SHA" ] && [ "$REMOTE_SHA" != "$LOCAL_SHA" ]; then echo "" fi -# Load .env if present +# ── 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 "" +fi + +# 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 +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'" + python3 - <"${POKE_TOKEN}"', content) +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 + python3 - <"${POKE_TOKEN_INPUT}"', content) +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 -q 'your-secret-key-here' .env 2>/dev/null || ! grep -q '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 - < .env + fi + echo " ✓ MCP_API_KEY generated and saved to .env" + echo "" +fi + +# ── Load .env ───────────────────────────────────────────────────────────────── if [ -f .env ]; then set -a source .env set +a fi -# Activate virtualenv -source .venv/bin/activate - : "${MCP_API_KEY:?MCP_API_KEY is not set — add it to .env or export it}" -# Start poke-mail server in background +# ── Start server + tunnel ───────────────────────────────────────────────────── echo "Starting poke-mail server..." python src/server.py & SERVER_PID=$! From 7e94e91753232423f020a08bacb89b0b573f768b Mon Sep 17 00:00:00 2001 From: 0xK Date: Tue, 24 Mar 2026 10:02:52 +0100 Subject: [PATCH 3/8] =?UTF-8?q?remove=20setup.sh=20=E2=80=94=20logic=20mov?= =?UTF-8?q?ed=20into=20start.sh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.sh | 147 ------------------------------------------------------- 1 file changed, 147 deletions(-) delete mode 100644 setup.sh diff --git a/setup.sh b/setup.sh deleted file mode 100644 index 7a2a09e..0000000 --- a/setup.sh +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env bash -# setup.sh — one-time setup for poke-mail -# Automates token creation via the Poke login SDK so you don't have to -# copy/paste API keys manually. -set -euo pipefail - -cd "$(dirname "$0")" - -echo "" -echo " poke-mail setup" -echo " ────────────────────────────────────────" -echo "" - -# ── 1. Python virtualenv ──────────────────────────────────────────────────── -if [ ! -d .venv ]; then - echo "Creating Python virtualenv (.venv)..." - python3 -m venv .venv -fi -source .venv/bin/activate -echo "Installing Python dependencies..." -pip install -q -r requirements.txt -echo " ✓ Python dependencies installed" -echo "" - -# ── 2. Poke API key (poke_api_key for config.yml) ──────────────────────────── -# The 'poke' npm package stores credentials at: -# ~/.config/poke/credentials.json → { "token": "..." } -# when the user runs: npx poke login - -POKE_CREDENTIALS_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/poke/credentials.json" -POKE_API_KEY_VALUE="" - -if [ -f "$POKE_CREDENTIALS_FILE" ]; then - # Try to extract .token with python (avoids jq dependency) - POKE_API_KEY_VALUE=$(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_API_KEY_VALUE" ]; then - echo " ✓ Found Poke credentials from 'poke login'" -else - echo " Poke API key not found via 'poke login'." - echo "" - echo " Option 1 (recommended): Run the following, then re-run this script:" - echo " npx poke login" - echo "" - echo " Option 2: Paste your API key from https://poke.com/settings/advanced" - echo "" - printf " Poke API key (leave blank to set later): " - read -r POKE_API_KEY_VALUE - POKE_API_KEY_VALUE=$(echo "$POKE_API_KEY_VALUE" | tr -d '[:space:]') -fi -echo "" - -# ── 3. config.yml ──────────────────────────────────────────────────────────── -if [ ! -f config.yml ]; then - echo "Copying config.example.yml → config.yml..." - cp config.example.yml config.yml - echo " ✓ config.yml created" -else - echo " ✓ config.yml already exists — skipping copy" -fi - -# Inject poke_api_key into config.yml if we have one -if [ -n "$POKE_API_KEY_VALUE" ]; then - # Replace the placeholder value in-place (handles both quoted forms) - python3 - <"${POKE_API_KEY_VALUE}"' -new_content = re.sub(pattern, replacement, content) - -with open('config.yml', 'w') as f: - f.write(new_content) - -print(' ✓ poke_api_key written to config.yml') -EOF -else - echo " ⚠ poke_api_key not set — edit config.yml manually before running start.sh" -fi -echo "" - -# ── 4. MCP_API_KEY (.env) ──────────────────────────────────────────────────── -if [ -f .env ] && grep -q 'MCP_API_KEY=' .env && ! grep -q 'MCP_API_KEY=your-secret-key-here' .env; then - echo " ✓ .env already has MCP_API_KEY — skipping" -else - echo "Generating MCP_API_KEY..." - 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 - # Update existing file - python3 - < .env - fi - echo " ✓ MCP_API_KEY generated and saved to .env" -fi -echo "" - -# ── 5. Poke npm package ─────────────────────────────────────────────────────── -if ! command -v poke &>/dev/null; then - echo "Installing the 'poke' npm package globally..." - npm install -g poke - echo " ✓ poke installed" -else - echo " ✓ poke CLI already installed ($(poke --version 2>/dev/null || echo 'version unknown'))" -fi -echo "" - -# ── Done ───────────────────────────────────────────────────────────────────── -echo " ────────────────────────────────────────" -echo " Setup complete!" -echo "" -if [ -z "$POKE_API_KEY_VALUE" ]; then - echo " Next steps:" - echo " 1. Run 'npx poke login' or add your Poke API key to config.yml" - echo " 2. Fill in your email credentials in config.yml" - echo " 3. Run ./start.sh" -else - echo " Next steps:" - echo " 1. Fill in your email credentials in config.yml" - echo " 2. Run ./start.sh" -fi -echo "" From 581dd0b89af5f414f8732e6dbfb02b928c8668b9 Mon Sep 17 00:00:00 2001 From: 0xK Date: Tue, 24 Mar 2026 11:40:26 +0100 Subject: [PATCH 4/8] fix: optional MCP_API_KEY in tunnel mode + address Copilot review issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- README.md | 18 +++++++++---- src/server.py | 20 +++++++++++--- start.sh | 74 +++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 87 insertions(+), 25 deletions(-) mode change 100755 => 100644 src/server.py diff --git a/README.md b/README.md index 91491e8..df24a41 100644 --- a/README.md +++ b/README.md @@ -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 `. -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 | diff --git a/src/server.py b/src/server.py old mode 100755 new mode 100644 index 3525d65..651380b --- a/src/server.py +++ b/src/server.py @@ -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) diff --git a/start.sh b/start.sh index 7032b37..a18634f 100644 --- a/start.sh +++ b/start.sh @@ -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 - <"${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 - <"${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 - </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..." -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 From ebef198131e019f687ae52c045a7de8e8a5915cb Mon Sep 17 00:00:00 2001 From: 0xK Date: Tue, 24 Mar 2026 11:46:32 +0100 Subject: [PATCH 5/8] feat: add OTA update logic to start.sh (git pull + requirements.txt check) --- start.sh | 48 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/start.sh b/start.sh index a18634f..1757e2e 100644 --- a/start.sh +++ b/start.sh @@ -3,18 +3,44 @@ set -euo pipefail cd "$(dirname "$0")" -# ── Check for updates ───────────────────────────────────────────────────────── -REPO="kacperkwapisz/poke-mail" -LOCAL_SHA=$(git rev-parse HEAD 2>/dev/null || echo "unknown") -REMOTE_SHA=$(curl -sf "https://api.github.com/repos/${REPO}/commits/main" \ - | grep -m1 '"sha"' | cut -d'"' -f4 || echo "") +# ── OTA update ──────────────────────────────────────────────────────────────── +# Pull latest changes from remote with a short timeout so we don't hang offline. +# If requirements.txt changed, reinstall dependencies afterwards. +if git rev-parse --is-inside-work-tree &>/dev/null 2>&1; then + echo "Checking for updates..." + REQS_BEFORE=$(git rev-parse HEAD:requirements.txt 2>/dev/null || echo "") -if [ -n "$REMOTE_SHA" ] && [ "$REMOTE_SHA" != "$LOCAL_SHA" ]; then - echo "⚡ A newer version of poke-mail is available." - echo " Local: ${LOCAL_SHA:0:7}" - echo " Remote: ${REMOTE_SHA:0:7}" - echo " Run 'git pull' to update." - echo "" + # git fetch with a 5-second timeout; silently skip if offline or unreachable + if git fetch --depth=1 origin --quiet --no-tags \ + -c core.sshCommand="ssh -o ConnectTimeout=5" \ + -c http.lowSpeedLimit=1 -c http.lowSpeedTime=5 \ + 2>/dev/null; then + LOCAL=$(git rev-parse HEAD) + REMOTE=$(git rev-parse FETCH_HEAD 2>/dev/null || echo "") + + if [ -n "$REMOTE" ] && [ "$LOCAL" != "$REMOTE" ]; then + echo " ↳ Update found (${LOCAL:0:7} → ${REMOTE:0:7}), applying..." + git merge --ff-only FETCH_HEAD --quiet + echo " ✓ Updated to $(git rev-parse --short HEAD)" + + # Re-check requirements.txt after update + REQS_AFTER=$(git rev-parse HEAD:requirements.txt 2>/dev/null || echo "") + if [ "$REQS_BEFORE" != "$REQS_AFTER" ]; then + echo " ↳ requirements.txt changed — reinstalling dependencies..." + # Activate venv if it already exists so pip targets the right env + [ -d .venv ] && source .venv/bin/activate + pip install -q -r requirements.txt + echo " ✓ Dependencies updated" + fi + echo "" + else + echo " ✓ Already up to date" + echo "" + fi + else + echo " ℹ Could not reach remote — continuing with local version." + echo "" + fi fi # ── One-time setup (skipped on subsequent runs) ─────────────────────────────── From e12a9bf784edcb4e61206ce1d7c6ba4de07a66b4 Mon Sep 17 00:00:00 2001 From: 0xK Date: Tue, 24 Mar 2026 11:58:30 +0100 Subject: [PATCH 6/8] fix: skip OTA update gracefully when git is not installed --- start.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/start.sh b/start.sh index 1757e2e..21a1cad 100644 --- a/start.sh +++ b/start.sh @@ -6,7 +6,9 @@ cd "$(dirname "$0")" # ── OTA update ──────────────────────────────────────────────────────────────── # Pull latest changes from remote with a short timeout so we don't hang offline. # If requirements.txt changed, reinstall dependencies afterwards. -if git rev-parse --is-inside-work-tree &>/dev/null 2>&1; then +if ! command -v git &>/dev/null; then + echo " ℹ git not found — skipping update check." +elif git rev-parse --is-inside-work-tree &>/dev/null 2>&1; then echo "Checking for updates..." REQS_BEFORE=$(git rev-parse HEAD:requirements.txt 2>/dev/null || echo "") From eb1bdd5683751db96856e47c9861a5a0639e2e7f Mon Sep 17 00:00:00 2001 From: 0xK Date: Tue, 24 Mar 2026 12:00:07 +0100 Subject: [PATCH 7/8] fix: auto-install git (brew/apt-get) if missing before OTA check --- start.sh | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/start.sh b/start.sh index 21a1cad..ba81280 100644 --- a/start.sh +++ b/start.sh @@ -4,11 +4,27 @@ set -euo pipefail cd "$(dirname "$0")" # ── OTA update ──────────────────────────────────────────────────────────────── +# Ensure git is available. If not, attempt a quiet install via brew (macOS) or +# apt-get (Linux). If the install fails or the OS is unsupported, skip OTA +# silently — never abort the script over a missing update tool. +if ! command -v git &>/dev/null; then + echo " ℹ git not found — attempting to install..." + _git_installed=0 + if command -v brew &>/dev/null; then + brew install git --quiet &>/dev/null && _git_installed=1 || true + elif command -v apt-get &>/dev/null; then + sudo apt-get install -y -qq git &>/dev/null && _git_installed=1 || true + fi + if [ "$_git_installed" -eq 1 ] && command -v git &>/dev/null; then + echo " ✓ git installed" + else + echo " ⚠ Could not install git — skipping update check." + fi +fi + # Pull latest changes from remote with a short timeout so we don't hang offline. # If requirements.txt changed, reinstall dependencies afterwards. -if ! command -v git &>/dev/null; then - echo " ℹ git not found — skipping update check." -elif git rev-parse --is-inside-work-tree &>/dev/null 2>&1; then +if command -v git &>/dev/null && git rev-parse --is-inside-work-tree &>/dev/null 2>&1; then echo "Checking for updates..." REQS_BEFORE=$(git rev-parse HEAD:requirements.txt 2>/dev/null || echo "") From a52989e7f16e099389fd691117bae48e270d2376 Mon Sep 17 00:00:00 2001 From: 0xK Date: Tue, 24 Mar 2026 12:02:45 +0100 Subject: [PATCH 8/8] feat: replace git-based OTA with curl + Python tarfile (no git dependency) --- start.sh | 143 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 90 insertions(+), 53 deletions(-) diff --git a/start.sh b/start.sh index ba81280..27a965e 100644 --- a/start.sh +++ b/start.sh @@ -4,59 +4,107 @@ set -euo pipefail cd "$(dirname "$0")" # ── OTA update ──────────────────────────────────────────────────────────────── -# Ensure git is available. If not, attempt a quiet install via brew (macOS) or -# apt-get (Linux). If the install fails or the OS is unsupported, skip OTA -# silently — never abort the script over a missing update tool. -if ! command -v git &>/dev/null; then - echo " ℹ git not found — attempting to install..." - _git_installed=0 - if command -v brew &>/dev/null; then - brew install git --quiet &>/dev/null && _git_installed=1 || true - elif command -v apt-get &>/dev/null; then - sudo apt-get install -y -qq git &>/dev/null && _git_installed=1 || true - fi - if [ "$_git_installed" -eq 1 ] && command -v git &>/dev/null; then - echo " ✓ git installed" - else - echo " ⚠ Could not install git — skipping update check." - fi -fi +# Uses curl + Python stdlib tarfile — no git or unzip required. +# +# Flow: +# 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 command -v curl &>/dev/null && command -v python3 &>/dev/null; then + _OTA_REPO="kacperkwapisz/poke-mail" + _OTA_BRANCH="main" + _VERSION_FILE=".poke_version" -# Pull latest changes from remote with a short timeout so we don't hang offline. -# If requirements.txt changed, reinstall dependencies afterwards. -if command -v git &>/dev/null && git rev-parse --is-inside-work-tree &>/dev/null 2>&1; then echo "Checking for updates..." - REQS_BEFORE=$(git rev-parse HEAD:requirements.txt 2>/dev/null || echo "") - # git fetch with a 5-second timeout; silently skip if offline or unreachable - if git fetch --depth=1 origin --quiet --no-tags \ - -c core.sshCommand="ssh -o ConnectTimeout=5" \ - -c http.lowSpeedLimit=1 -c http.lowSpeedTime=5 \ - 2>/dev/null; then - LOCAL=$(git rev-parse HEAD) - REMOTE=$(git rev-parse FETCH_HEAD 2>/dev/null || echo "") + # 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 "") - if [ -n "$REMOTE" ] && [ "$LOCAL" != "$REMOTE" ]; then - echo " ↳ Update found (${LOCAL:0:7} → ${REMOTE:0:7}), applying..." - git merge --ff-only FETCH_HEAD --quiet - echo " ✓ Updated to $(git rev-parse --short HEAD)" + _LOCAL_SHA=$(cat "$_VERSION_FILE" 2>/dev/null || echo "") - # Re-check requirements.txt after update - REQS_AFTER=$(git rev-parse HEAD:requirements.txt 2>/dev/null || echo "") - if [ "$REQS_BEFORE" != "$REQS_AFTER" ]; then + 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-/" + 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..." - # Activate venv if it already exists so pip targets the right env [ -d .venv ] && source .venv/bin/activate pip install -q -r requirements.txt echo " ✓ Dependencies updated" fi - echo "" else - echo " ✓ Already up to date" - echo "" + echo " ℹ Download failed — continuing with local version." fi - else - echo " ℹ Could not reach remote — continuing with local version." + + rm -f "$_TMP_TAR" echo "" fi fi @@ -107,8 +155,6 @@ except Exception: if [ -n "$POKE_TOKEN" ]; then echo " ✓ Poke API key detected from 'poke login'" - # 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'] @@ -150,9 +196,6 @@ PYEOF fi # 4. MCP_API_KEY — generate once and persist to .env -# 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 @@ -162,7 +205,6 @@ alphabet = string.ascii_letters + string.digits print(''.join(secrets.choice(alphabet) for _ in range(48))) ") if [ -f .env ]; then - # 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'] @@ -189,14 +231,11 @@ if [ -f .env ]; then 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 +POKE_TUNNEL="${POKE_TUNNEL:-1}" 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 @@ -209,10 +248,8 @@ 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 — 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"