Switch default to .json + POST, implement dual-mode API client The Foal server uses the endpoint suffix to determine how to parse the request body: .lua = GET with query params, .json = POST with JSON body. Both route to the same handlers. - Default PONYMAIL_API_SUFFIX to ".json" (native Foal protocol) - apiFetch now automatically uses POST+JSON when suffix is ".json" and GET+params when ".lua" - All raw fetch calls (get_source, auth_status) also handle both modes - Update README and TODO to reflect the new default Set PONYMAIL_API_SUFFIX=.lua only for older deployments that lack the .json route registration.
diff --git a/mcp/ponymail-mcp/README.md b/mcp/ponymail-mcp/README.md index 8424d23..405900a 100644 --- a/mcp/ponymail-mcp/README.md +++ b/mcp/ponymail-mcp/README.md
@@ -38,7 +38,7 @@ | Variable | Default | Description | |----------|---------|-------------| | `PONYMAIL_BASE_URL` | `https://lists.apache.org` | Base URL of the PonyMail instance | -| `PONYMAIL_API_SUFFIX` | `.lua` | API endpoint suffix. Use `.lua` (default) for `lists.apache.org` or `.json` for native Foal deployments. | +| `PONYMAIL_API_SUFFIX` | `.json` | API endpoint suffix and method selector. `.json` (default) = POST with JSON body (native Foal). `.lua` = GET with query params (legacy compat for older deployments). | | `PONYMAIL_SESSION_COOKIE` | *(none)* | Manual session cookie override (skips OAuth flow) | | `PONYMAIL_RESTRICTED_LISTS` | *(see below)* | Comma-separated patterns to block pre-fetch. Set to `none` to clear pattern blocks. | | `PONYMAIL_ALLOWED_LISTS` | *(none)* | Comma-separated opt-in patterns. Lists matching these bypass all blocks. |
diff --git a/mcp/ponymail-mcp/index.js b/mcp/ponymail-mcp/index.js index 5159a68..9c531b7 100644 --- a/mcp/ponymail-mcp/index.js +++ b/mcp/ponymail-mcp/index.js
@@ -34,9 +34,9 @@ const BASE_URL = process.env.PONYMAIL_BASE_URL || "https://lists.apache.org"; -// API endpoint suffix. The legacy compat layer uses ".lua" (default), -// while native Foal deployments use ".json". -const API_SUFFIX = process.env.PONYMAIL_API_SUFFIX || ".lua"; +// API endpoint suffix and method selector. ".json" (default) uses POST with +// JSON body (native Foal). ".lua" uses GET with query params (legacy compat). +const API_SUFFIX = process.env.PONYMAIL_API_SUFFIX || ".json"; // Loud, opt-in security notice. When the user has enabled the auto-extract // path via PONYMAIL_AUTO_EXTRACT_COOKIE=1, remind them at every startup what @@ -67,24 +67,37 @@ // --------------------------------------------------------------------------- async function apiFetch(path, params = {}) { - const url = new URL(path, BASE_URL); - for (const [k, v] of Object.entries(params)) { - if (v !== undefined && v !== null && v !== "") { - url.searchParams.set(k, String(v)); - } - } - // 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 }); + let resp; + + if (API_SUFFIX === ".json") { + // .json endpoints expect POST with JSON body + const url = new URL(path, BASE_URL); + headers["Content-Type"] = "application/json"; + const body = {}; + for (const [k, v] of Object.entries(params)) { + if (v !== undefined && v !== null && v !== "") { + body[k] = v; + } + } + resp = await fetch(url.toString(), { method: "POST", headers, body: JSON.stringify(body) }); + } else { + // .lua endpoints use GET with query parameters + const url = new URL(path, BASE_URL); + for (const [k, v] of Object.entries(params)) { + if (v !== undefined && v !== null && v !== "") { + url.searchParams.set(k, String(v)); + } + } + resp = await fetch(url.toString(), { headers }); + } if (!resp.ok) { const body = await resp.text().catch(() => ""); @@ -479,13 +492,24 @@ // Fetch the raw source const url = new URL(`/api/source${API_SUFFIX}`, BASE_URL); - url.searchParams.set("id", id); const headers = { Accept: "text/plain" }; const envCookie = process.env.PONYMAIL_SESSION_COOKIE; const sessionCookie = envCookie || loadSession(); if (sessionCookie) headers.Cookie = sessionCookie; - const resp = await fetch(url.toString(), { headers }); + let resp; + if (API_SUFFIX === ".json") { + headers["Content-Type"] = "application/json"; + resp = await fetch(url.toString(), { + method: "POST", + headers, + body: JSON.stringify({ id }), + }); + } else { + url.searchParams.set("id", id); + resp = await fetch(url.toString(), { headers }); + } + if (!resp.ok) { const body = await resp.text().catch(() => ""); throw new Error(`PonyMail API error ${resp.status}: ${body}`); @@ -651,9 +675,18 @@ // Validate the cookie try { const url = new URL(`/api/preferences${API_SUFFIX}`, BASE_URL); - const resp = await fetch(url.toString(), { - headers: { Accept: "application/json", Cookie: sessionCookie }, - }); + const headers = { Accept: "application/json", Cookie: sessionCookie }; + let resp; + if (API_SUFFIX === ".json") { + headers["Content-Type"] = "application/json"; + resp = await fetch(url.toString(), { + method: "POST", + headers, + body: JSON.stringify({}), + }); + } else { + resp = await fetch(url.toString(), { headers }); + } const data = await resp.json(); if (data.login && data.login.credentials) {