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) {