Removes all access to private lists.
diff --git a/mcp/ponymail-mcp/README.md b/mcp/ponymail-mcp/README.md index e599320..7e21221 100644 --- a/mcp/ponymail-mcp/README.md +++ b/mcp/ponymail-mcp/README.md
@@ -1,6 +1,6 @@ # PonyMail MCP Server -An MCP (Model Context Protocol) server that provides access to the [Apache PonyMail](https://ponymail.apache.org/) mailing list archive API. +An MCP (Model Context Protocol) server that provides access to the [Apache PonyMail](https://ponymail.apache.org/) mailing list archive API. Public lists only. ## Tools @@ -11,9 +11,6 @@ | `get_email` | Fetch a specific email by ID with full body and attachments | | `get_thread` | Fetch the root message of a thread by thread ID | | `get_mbox` | Download mbox-formatted archive data for bulk export | -| `login` | Authenticate via ASF OAuth to access private mailing lists | -| `logout` | Clear cached session cookie | -| `auth_status` | Check current authentication status | ## Setup @@ -36,38 +33,6 @@ | Variable | Default | Description | |----------|---------|-------------| | `PONYMAIL_BASE_URL` | `https://lists.apache.org` | Base URL of the PonyMail instance | -| `PONYMAIL_SESSION_COOKIE` | *(none)* | Manual session cookie override (skips OAuth flow) | - -## Authentication (Private Lists) - -Public lists work without authentication. For private/restricted lists, you have two options: - -### Option 1: OAuth via Login Tool (Recommended) - -Use the `login` tool from within your MCP client. It will: - -1. Open a local helper page at `http://localhost:39817` -2. The page links to PonyMail's login page — log in with your ASF LDAP credentials -3. After logging in, grab the session cookie (see below) and paste it into the form -4. The server validates the cookie and caches it to `~/.ponymail-mcp/session.json` - -**Finding the HttpOnly cookie:** The `ponymail` cookie is `HttpOnly`, so `document.cookie` and the Application tab won't show it. To find it: -1. On `lists.apache.org` (while logged in), open DevTools (`Cmd+Option+I` / `F12`) -2. Go to the **Network** tab and reload the page -3. Click on any request (e.g., the page itself, or any `api/` call) -4. In **Headers** → **Request Headers** → find the **Cookie:** line -5. Copy the `ponymail=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` part -### Option 2: Manual Cookie - -1. Log into https://lists.apache.org in your browser -2. Open DevTools → Application → Cookies → copy the session cookie -3. Set the environment variable: - ``` - PONYMAIL_SESSION_COOKIE="ponymail=abc123..." - ``` -4. Add it to your MCP server config's environment variables - -Sessions expire after ~20 hours. Use `auth_status` to check, `logout` to clear. ## Usage Examples
diff --git a/mcp/ponymail-mcp/auth.js b/mcp/ponymail-mcp/auth.js deleted file mode 100644 index 5fe14fe..0000000 --- a/mcp/ponymail-mcp/auth.js +++ /dev/null
@@ -1,261 +0,0 @@ -/** - * auth.js — PonyMail session management - * - * PonyMail Foal handles OAuth entirely server-side — the auth code from ASF OAuth - * can only be exchanged by PonyMail's own backend (its redirect_uri is registered - * with ASF OAuth, not ours). So we can't replicate the OAuth exchange from a CLI. - * - * Instead, this module: - * 1. Opens the PonyMail login page in the user's browser - * 2. Runs a tiny local server that waits for the user to paste their cookie - * OR watches for the cookie file to appear (if using browser extension) - * 3. Caches the session cookie to ~/.ponymail-mcp/session.json - * - * The simplest reliable flow: - * - Open lists.apache.org/oauth.html in the browser - * - User logs in (ASF LDAP) - * - After login, PonyMail sets a session cookie in the browser - * - User copies the cookie value from DevTools (or we provide a bookmarklet) - * - We cache it and use it for API requests - * - * Alternatively, set PONYMAIL_SESSION_COOKIE env var directly. - */ - -import http from "node:http"; -import fs from "node:fs"; -import path from "node:path"; -import os from "node:os"; -import { exec } from "node:child_process"; - -const SESSION_DIR = path.join(os.homedir(), ".ponymail-mcp"); -const SESSION_FILE = path.join(SESSION_DIR, "session.json"); -const CALLBACK_PORT = 39817; -const LOGIN_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes - -// --------------------------------------------------------------------------- -// Session persistence -// --------------------------------------------------------------------------- - -export function loadSession() { - try { - if (!fs.existsSync(SESSION_FILE)) return null; - const data = JSON.parse(fs.readFileSync(SESSION_FILE, "utf-8")); - if (data.timestamp && Date.now() - data.timestamp > 20 * 60 * 60 * 1000) { - console.error("[auth] Cached session expired"); - return null; - } - return data.cookie || null; - } catch { - return null; - } -} - -function saveSession(cookie, userInfo = {}) { - fs.mkdirSync(SESSION_DIR, { recursive: true }); - fs.writeFileSync( - SESSION_FILE, - JSON.stringify({ cookie, timestamp: Date.now(), user: userInfo }, null, 2) - ); - console.error(`[auth] Session saved to ${SESSION_FILE}`); -} - -export function clearSession() { - try { - if (fs.existsSync(SESSION_FILE)) { - fs.unlinkSync(SESSION_FILE); - console.error("[auth] Session cleared"); - } - } catch (err) { - console.error("[auth] Failed to clear session:", err.message); - } -} - -// --------------------------------------------------------------------------- -// Browser helper -// --------------------------------------------------------------------------- - -function openBrowser(url) { - const cmd = - process.platform === "darwin" - ? `open "${url}"` - : process.platform === "win32" - ? `start "${url}"` - : `xdg-open "${url}"`; - exec(cmd, (err) => { - if (err) console.error("[auth] Could not open browser:", err.message); - }); -} - -// --------------------------------------------------------------------------- -// Login flow -// --------------------------------------------------------------------------- - -/** - * Perform login by: - * 1. Opening PonyMail's login page in the browser - * 2. Starting a local HTTP server with a simple form where the user pastes - * their cookie after logging in, OR uses the bookmarklet to auto-fill - * 3. Saving the cookie once received - * - * @param {string} baseUrl - PonyMail base URL - * @param {number} [timeoutMs] - Max time to wait (default 3 min) - * @returns {Promise<string>} The session cookie string - */ -export function performLogin(baseUrl, timeoutMs = LOGIN_TIMEOUT_MS) { - return new Promise((resolve, reject) => { - let server; - let settled = false; - - function settle(err, result) { - if (settled) return; - settled = true; - clearTimeout(timer); - if (server) { - try { server.close(); } catch {} - } - if (err) reject(err); - else resolve(result); - } - - const timer = setTimeout(() => { - settle(new Error( - `Login timed out after ${timeoutMs / 1000}s. Call login again to retry.` - )); - }, timeoutMs); - - server = http.createServer(async (req, res) => { - // Serve the cookie-paste form - if (req.method === "GET") { - res.writeHead(200, { "Content-Type": "text/html" }); - res.end(loginPage(baseUrl)); - return; - } - - // Receive the pasted cookie - if (req.method === "POST" && req.url === "/save") { - let body = ""; - for await (const chunk of req) body += chunk; - const params = new URLSearchParams(body); - const cookie = (params.get("cookie") || "").trim(); - - if (!cookie) { - res.writeHead(400, { "Content-Type": "text/html" }); - res.end(resultPage(false, "No cookie value provided. Please try again.")); - return; - } - - // Validate the cookie by calling preferences endpoint - try { - const testUrl = new URL("/api/preferences.lua", baseUrl); - const testResp = await fetch(testUrl.toString(), { - headers: { Accept: "application/json", Cookie: cookie }, - }); - const testData = await testResp.json(); - - if (testData.login && testData.login.credentials) { - const name = testData.login.credentials.fullname || "Unknown"; - const email = testData.login.credentials.email || ""; - saveSession(cookie, { fullname: name, email }); - res.writeHead(200, { "Content-Type": "text/html" }); - res.end(resultPage(true, `Authenticated as ${name} (${email})`)); - settle(null, cookie); - } else { - res.writeHead(200, { "Content-Type": "text/html" }); - res.end(resultPage(false, - "Cookie was accepted but no login credentials found. " + - "Make sure you copied the full cookie string. Try again." - )); - } - } catch (err) { - res.writeHead(500, { "Content-Type": "text/html" }); - res.end(resultPage(false, `Validation failed: ${err.message}`)); - } - return; - } - - res.writeHead(404); - res.end("Not found"); - }); - - server.on("error", (err) => { - settle(new Error(`Could not start callback server on port ${CALLBACK_PORT}: ${err.message}`)); - }); - - server.listen(CALLBACK_PORT, () => { - // Open the local helper page (which has a link to PonyMail login + paste form) - const helperUrl = `http://localhost:${CALLBACK_PORT}`; - console.error(`[auth] Opening login helper at ${helperUrl}`); - openBrowser(helperUrl); - }); - }); -} - -// --------------------------------------------------------------------------- -// HTML pages -// --------------------------------------------------------------------------- - -function loginPage(baseUrl) { - const oauthUrl = `${baseUrl}/oauth.html`; - const hostname = new URL(baseUrl).hostname; - return `<!DOCTYPE html> -<html> -<head><title>PonyMail MCP Login</title></head> -<body style="font-family:system-ui;max-width:640px;margin:40px auto;padding:0 20px"> - <h1>🐴 PonyMail MCP Login</h1> - - <p><strong>Step 1:</strong> Log into PonyMail (if you haven't already):</p> - <p><a href="${oauthUrl}" target="_blank" style="font-size:1.2em;color:#0066cc"> - ➜ Open ${oauthUrl} - </a></p> - - <p><strong>Step 2:</strong> After logging in, get the session cookie. The cookie is HttpOnly, so you need to find it via DevTools:</p> - <ol style="line-height:1.8"> - <li>On <code>${hostname}</code>, open DevTools: <code>Cmd+Option+I</code> (or <code>F12</code>)</li> - <li>Go to the <strong>Network</strong> tab</li> - <li>Reload the page (or click any link)</li> - <li>Click on any request to <code>${hostname}</code> (e.g., the document or any <code>api/</code> call)</li> - <li>In <strong>Headers</strong> → <strong>Request Headers</strong> → find the <strong>Cookie:</strong> line</li> - <li>Copy the <code>ponymail=xxxxxxxx-xxxx-...</code> part</li> - </ol> - <p>It typically looks like:<br> - <code style="background:#f0f0f0;padding:2px 6px">ponymail=abc123def456...</code></p> - - <p><strong>Step 3:</strong> Paste it below:</p> - <form method="POST" action="/save"> - <input name="cookie" type="text" placeholder="ponymail=..." - style="width:100%;padding:10px;font-size:1em;font-family:monospace;border:1px solid #ccc;border-radius:4px" /> - <br><br> - <button type="submit" - style="padding:10px 24px;font-size:1em;background:#0066cc;color:white;border:none;border-radius:4px;cursor:pointer"> - Save Cookie - </button> - </form> - - <details style="margin-top:30px"> - <summary style="cursor:pointer;color:#0066cc">💡 Quick alternative: Console one-liner</summary> - <p>On <code>${hostname}</code>, open DevTools Console and run:</p> - <pre style="background:#f5f5f5;padding:12px;border-radius:4px;overflow-x:auto;font-size:0.85em">fetch('/api/preferences.lua').then(r=>r.json()).then(j=>console.log(j.login?.credentials ? '✅ Logged in as: '+j.login.credentials.fullname+'\\nNow check Network tab for any api/ request → Request Headers → Cookie' : '❌ Not logged in — log in first'))</pre> - <p>This confirms you're logged in and triggers a network request so the cookie appears in the Network tab.</p> - </details> - - <p style="color:#888;margin-top:30px;font-size:0.9em"> - The cookie will be saved to <code>~/.ponymail-mcp/session.json</code> and used for API requests. - This page will close automatically after saving. Session expires after ~20 hours. - </p> -</body> -</html>`; -} - -function resultPage(success, message) { - const icon = success ? "✅" : "❌"; - const color = success ? "#2e7d32" : "#c62828"; - return `<!DOCTYPE html> -<html> -<head><title>PonyMail MCP Login</title></head> -<body style="font-family:system-ui;max-width:640px;margin:40px auto;padding:0 20px;text-align:center"> - <h1 style="color:${color}">${icon} ${success ? "Authenticated!" : "Not Authenticated"}</h1> - <p>${message}</p> - ${success ? "<p>You can close this tab and return to Amazon Quick.</p>" : '<p><a href="/">← Try again</a></p>'} -</body> -</html>`; -}
diff --git a/mcp/ponymail-mcp/bookmarklet.html b/mcp/ponymail-mcp/bookmarklet.html deleted file mode 100644 index 57b150d..0000000 --- a/mcp/ponymail-mcp/bookmarklet.html +++ /dev/null
@@ -1,31 +0,0 @@ -<!DOCTYPE html> -<html> -<head><title>PonyMail Cookie Bookmarklet</title></head> -<body style="font-family:system-ui;max-width:700px;margin:40px auto;padding:0 20px"> -<h1>🐴 PonyMail Cookie Extractor</h1> -<p>Drag this bookmarklet to your bookmarks bar:</p> - -<p style="text-align:center;margin:30px 0"> -<a href="javascript:void(fetch('/api/preferences.lua',{credentials:'include'}).then(r=>{const c=document.cookie;return r.json().then(j=>{const m=j.login&&j.login.credentials;if(!m){alert('Not logged in to PonyMail. Log in first, then try again.');return}const x=new XMLHttpRequest();x.open('GET','/api/preferences.lua',false);x.withCredentials=true;x.send();const raw=x.getResponseHeader('X-Request-Cookies')||'';let pc='';document.cookie.split(';').forEach(s=>{if(s.trim().startsWith('ponymail='))pc=s.trim()});if(!pc){const p=prompt('Logged in as: '+m.fullname+' ('+m.email+')\n\nThe ponymail cookie is HttpOnly so I cannot read it directly.\n\nPlease paste the cookie value from:\nDevTools → Network → any api/ request → Headers → Cookie\n\nLook for: ponymail=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx','');if(p){const v=p.startsWith('ponymail=')?p:'ponymail='+p;navigator.clipboard.writeText(v).then(()=>alert('Copied to clipboard:\n'+v)).catch(()=>prompt('Copy this:',v))}return}navigator.clipboard.writeText(pc).then(()=>alert('Cookie copied to clipboard!\n\n'+pc)).catch(()=>prompt('Copy this cookie value:',pc))})}).catch(e=>alert('Error: '+e.message)))" - style="display:inline-block;padding:12px 24px;background:#0066cc;color:white;text-decoration:none;border-radius:6px;font-size:1.1em;cursor:grab"> - 📋 Get PonyMail Cookie -</a> -</p> - -<h2>How to use</h2> -<ol> - <li>Drag the blue button above to your <strong>bookmarks bar</strong></li> - <li>Go to <a href="https://lists.apache.org">lists.apache.org</a> and <strong>log in</strong></li> - <li>Click the bookmarklet</li> - <li>It will confirm you're logged in and prompt for the cookie value</li> - <li>The quickest way to get it: before clicking the bookmarklet, open DevTools (<code>Cmd+Option+I</code>) → <strong>Network</strong> tab, then click the bookmarklet. Look at the <code>preferences.lua</code> request → <strong>Request Headers</strong> → <strong>Cookie</strong> line</li> - <li>Copy just the <code>ponymail=xxxxxxxx-xxxx-...</code> part and paste it in the prompt</li> - <li>The bookmarklet copies the formatted cookie to your clipboard</li> - <li>Paste it into your MCP server's <code>PONYMAIL_SESSION_COOKIE</code> env var</li> -</ol> - -<h2>Even simpler: Console one-liner</h2> -<p>On <code>lists.apache.org</code>, open DevTools Console and run:</p> -<pre style="background:#f5f5f5;padding:12px;border-radius:4px;overflow-x:auto">fetch('/api/preferences.lua').then(r=>r.json()).then(j=>{if(j.login?.credentials){console.log('✅ Logged in as:',j.login.credentials.fullname);console.log('Now check the Network tab for preferences.lua request → Cookie header → copy the ponymail=... value')}else{console.log('❌ Not logged in')}})</pre> -</body> -</html>
diff --git a/mcp/ponymail-mcp/index.js b/mcp/ponymail-mcp/index.js index 100fddd..27ba605 100644 --- a/mcp/ponymail-mcp/index.js +++ b/mcp/ponymail-mcp/index.js
@@ -3,7 +3,6 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; -import { loadSession, performLogin, clearSession } from "./auth.js"; const BASE_URL = process.env.PONYMAIL_BASE_URL || "https://lists.apache.org"; @@ -19,17 +18,9 @@ } } - // Build headers — include session cookie if available - const headers = { Accept: "application/json" }; - - // Priority: env var > cached session file - const envCookie = process.env.PONYMAIL_SESSION_COOKIE; - const sessionCookie = envCookie || loadSession(); - if (sessionCookie) { - headers.Cookie = sessionCookie; - } - - const resp = await fetch(url.toString(), { headers }); + const resp = await fetch(url.toString(), { + headers: { Accept: "application/json" }, + }); if (!resp.ok) { const body = await resp.text().catch(() => ""); @@ -213,9 +204,6 @@ id: z.string().describe("The thread ID (tid)"), }, async ({ id }) => { - // Use stats endpoint scoped to a very wide range and filter by tid - // PonyMail doesn't have a dedicated thread endpoint, but we can fetch - // the root email which contains thread_struct, then fetch each child. const root = await apiFetch("/api/email.lua", { id }); const lines = []; @@ -260,77 +248,6 @@ } ); -// --- Tool: login ------------------------------------------------------------ -server.tool( - "login", - "Authenticate with Apache's OAuth system to access private mailing lists. " + - "Opens a browser window for ASF LDAP login. The session cookie is cached " + - "to ~/.ponymail-mcp/session.json and reused for subsequent requests.", - {}, - async () => { - // Check if already logged in - const existing = loadSession(); - if (existing) { - return { - content: [ - { - type: "text", - text: "Already authenticated (cached session found). Use `logout` first to re-authenticate, or `auth_status` to check details.", - }, - ], - }; - } - - try { - const cookie = await performLogin(BASE_URL); - return { - content: [ - { - type: "text", - text: `✅ Successfully authenticated! Session cookie cached.\nCookie: ${cookie.split("=")[0]}=...\nPrivate list access is now enabled.`, - }, - ], - }; - } catch (err) { - return { - content: [ - { type: "text", text: `❌ Authentication failed: ${err.message}` }, - ], - }; - } - } -); - -// --- Tool: logout ----------------------------------------------------------- -server.tool( - "logout", - "Clear the cached PonyMail session cookie. After this, only public lists will be accessible.", - {}, - async () => { - clearSession(); - return { - content: [{ type: "text", text: "Session cleared. Only public lists are now accessible." }], - }; - } -); - -// --- Tool: auth_status ------------------------------------------------------ -server.tool( - "auth_status", - "Check whether an authenticated session exists and display session info.", - {}, - async () => { - const cookie = process.env.PONYMAIL_SESSION_COOKIE || loadSession(); - const source = process.env.PONYMAIL_SESSION_COOKIE ? "environment variable" : "cached session file"; - const status = cookie - ? `✅ Authenticated (via ${source})\nCookie: ${cookie.split("=")[0]}=...` - : "❌ Not authenticated. Use the `login` tool to authenticate, or set PONYMAIL_SESSION_COOKIE env var."; - return { - content: [{ type: "text", text: status }], - }; - } -); - // --------------------------------------------------------------------------- // Start // ---------------------------------------------------------------------------