|
1 | 1 | ## Last session |
| 2 | +2026-08-18, later (**Mark's traffic capture killed my own diagnosis, and exposed two instruments that could not answer the question they exist for → rc7, LIVE on the channel**) — Fahim forwarded a clean ChatGPT reconnection recorded with the client-traffic recorder running. Issues [#120](https://github.com/plugpressco/saddle/issues/120) and [#121](https://github.com/plugpressco/saddle/issues/121) filed and closed, PRs [#122](https://github.com/plugpressco/saddle/pull/122)/[#123](https://github.com/plugpressco/saddle/pull/123)/[#124](https://github.com/plugpressco/saddle/pull/124) merged, **603 tests** (was 596), 0 lint errors. **rc7 published to R2 and verified live.** |
| 3 | + |
| 4 | +**Retraction, and it was mine.** Earlier today I told Fahim the MCP-Adapter shim chain (#111/#112) was Mark's bug. It is not. His own report says `Transport: Saddle built-in JSON-RPC` — he does not have that plugin, so that code path never runs on his site. rc6 was worth shipping and was never his fix. *(Second correction in one day: I also wrote here that rc5 was never published. It was — the live channel was serving it. Both errors came from asserting before checking, once against a stale git ref and once against a file I had not read.)* |
| 5 | + |
| 6 | +**What his capture actually proves.** ChatGPT's client (`Python/3.12 aiohttp/3.13.5`) made exactly two requests, both **401 at the transport gate**, with no `initialize` and no `tools/list` anywhere in the window; the browser half of OAuth returned 200 throughout. So this is authentication, full stop — not discovery, schemas, tiers, or the handshake. ChatGPT saves the connector because the browser flow succeeded and shows zero actions because its client never got in. |
| 7 | + |
| 8 | +**Two instruments, neither able to say why — both fixed.** |
| 9 | +- **[#121](https://github.com/plugpressco/saddle/issues/121), the one that mattered.** `probe_headers()` sent ONE loopback and it carried Basic. Basic is the scheme that survives where others don't: Apache/LiteSpeed consume RFC 7617 natively into `PHP_AUTH_USER`, so it reaches PHP even where the raw header is never forwarded, and a WAF rule can target `Bearer` alone. **So `self_check()` could report `auth_header: ok` on a site refusing every OAuth client** — exactly "Claude works, ChatGPT 401s". Application Passwords carry Basic (Claude Code/Desktop, Cursor, VS Code); ChatGPT has no field for a custom header, so Bearer is its only path. *"Claude works, so the header is fine"* is the wrong inference and this function was licensing it. Now two probes, judged on the scheme that **arrived**, with `bearer_header`, a `bearer_header_stripped` status, `htaccess_fixable` extended (the rule always forwarded the whole header — it just was never offered), and its own UI copy, because telling someone their pasted-key apps are broken when they aren't sends them hunting the wrong thing. |
| 10 | +- **[#120](https://github.com/plugpressco/saddle/issues/120), why his capture was mostly noise.** `targets_mcp()` was a bare `strpos` prefix test and the admin API shares `saddle/v1`, so `/saddle/v1/mcp-diagnostics` matched — and the panel polls it every 5s while recording. At 25 entries the buffer turned over every **~125 seconds**; his window was **114**, and 23 of 25 rows were the panel watching itself. The instrument was evicting its own evidence. Also: `scheme` had been recorded on every row since the recorder shipped and **rendered nowhere**, so the field separating "key rejected" from "no key arrived" — same 401, opposite fixes — was written to the option and shown to no one. Mark asked us for it by name while it sat in `saddle_mcp_trace` on his own site. |
| 11 | + |
| 12 | +**Two tests that were passing for the wrong reason, caught and fixed.** `test_a_recorded_request_contains_no_credential` set an `Authorization` header on a synthetic `WP_REST_Request`, which populates no `$_SERVER` — and `Saddle_Connection` reads `$_SERVER` deliberately, because the failure being diagnosed is a strip *before PHP*. The recorder never saw a credential, so "no leak" was vacuous. And my own new `targets_mcp` regression **passed against the bug** on first write, because `rest_do_request()` never fires `rest_post_dispatch` so nothing was written either way; rewritten to drive the real chain. Every new test in both PRs was then verified red without its fix. |
| 13 | + |
| 14 | +**rc7 is live and reaches people.** `/v1/update` → 1.0.0-rc7 with a signed package; the zip downloaded **through the live link** is byte-identical to the local build (sha256 `8ea73d41…`, 651,504 bytes) and **contains `class-saddle-updater.php`** — verified by unzipping what the URL returns rather than what was uploaded, because a channel serving an updater-less zip silently ends updates for every install on it. Manifest committed to `plugin-update-workers` (`7bbd0f4`). Both artifacts 120/121 files, one file apart. |
| 15 | + |
| 16 | +## Previous session |
2 | 17 | 2026-08-18 (**Mark's second email read against the tree → rc6 cut with the fix rc5 missed, plus two defects found in the diagnostic we were about to point him at**) — Fahim forwarded Mark's clarification. Three PRs merged ([#116](https://github.com/plugpressco/saddle/pull/116), [#117](https://github.com/plugpressco/saddle/pull/117), [#118](https://github.com/plugpressco/saddle/pull/118)), issues #113 and #115 filed and closed, **596 tests** (was 594), 0 lint errors. **`dist/saddle-1.0.0-rc6{,-selfhosted}.zip` built and verified; NOT published to R2 and not yet sent to Mark.** |
3 | 18 |
|
4 | 19 | **What the email actually establishes.** ChatGPT never touched WordPress. It planned and wrote prompts; **Claude Desktop did every WordPress action through Saddle**, creating two Divi 5 posts out of Text and Code modules that stay editable in the Visual Builder, on staging and then live, with correct post ids, statuses and scheduled dates read back. First customer-side proof of the Divi path end to end, and it independently clears the endpoint, OAuth, tiers, the write gate and Pro's Divi layer. [#108](https://github.com/plugpressco/saddle/pull/108) is confirmed working in the field: his connection reads *Verified / Reading & writing* rather than pinned to read. The open bug is one narrow thing — ChatGPT completes setup and gets **0 callable tools**. |
@@ -293,11 +308,16 @@ Earlier 2026-07-12 — **Audit-backlog execution (P0–P4)**. Cleared the audit |
293 | 308 | - Earlier same day: the Closed-Loop Quality Engine free half (epic #22, PRs #28–#32, 300 green) — see git history. |
294 | 309 |
|
295 | 310 | ## Next up |
296 | | -- **Mark Roach — send rc6 and ask for ONE thing: the Client traffic report.** Saddle → Connections → Connection details & health → Client traffic → **Record the next hour** → reconnect in ChatGPT → **Copy report**. Its `Transport:` line decides everything: `MCP Adapter x.y.z (another plugin)` means #111 was his bug and rc6 fixes it; `Saddle built-in JSON-RPC` means look at the rows instead — no rows at all = the requests never reached WordPress (20i StackProtect / StackCache, or ChatGPT failing before it calls); `status:401` = the bearer isn't resolving; `status:200 tools:0` = our filter is withholding everything; `status:200 tools:36` = **Saddle answered correctly and ChatGPT dropped the list**, the [documented](https://community.openai.com/t/mcp-connected-but-not-invokable/1384847) connector bug. Tell him plainly that **0 tools is not the Business/Enterprise write gate** — a read-gated connector still lists read tools. Attach `dist/saddle-1.0.0-rc6-selfhosted.zip` (the `.org` zip has no updater, so he'd be hand-installing forever). **Still ask him to rotate the mrr.org.uk credentials he emailed in plaintext.** |
297 | | -- **rc6 is built but NOT published.** `./scripts/release.sh saddle 1.0.0-rc6 dist/saddle-1.0.0-rc6-selfhosted.zip <manifest>` pushes it to R2, which changes what every self-hosted install downloads — Fahim's call, deliberately not taken. Note rc5 was never published either, which is why the update channel is still behind. |
298 | | -- **The premise to settle before believing any of the above.** The #111 chain requires the official **MCP Adapter plugin** to be active on staging.kesuk.net. Nothing in hand confirms it. If the report says `Saddle built-in JSON-RPC`, the diagnosis is wrong and the rows are the evidence to reason from instead — don't retrofit. |
299 | | -- ~~**Mark Roach — reply drafted 2026-08-17, awaiting Fahim's send.**~~ *(superseded; the delivery-failure framing and the five answers still stand)* He escalated: two weeks, no working connection, and his complaint is explicitly about *communication* rather than bugs. **The uncomfortable finding: his bug was fixed on 08-16 and the build never reached him** — rc4 carried the handshake fix, but the update channel served rc3 until rc5 went out today, and this very line sat here saying "reply with a build to test" for a day and a half. Draft is in `~/.claude/plans/warm-juggling-babbage.md`: owns the delivery failure, answers his five questions, attaches `saddle-1.0.0-rc5-selfhosted.zip`, moves him to Fahim's personal address (support@plugpress.co queues), offers a refund with no strings, and asks him to rotate the plaintext mrr.org.uk credentials. **Open item the reply promises within 24h: settle whether OpenAI really gates write-capable connectors to Business/Enterprise/Edu** — their own docs contradict the secondary sources, and Mark's workspace is the live test case. Ask him the plan; record the answer here. |
300 | | -- ~~**Reply to Mark Roach with a build to test.**~~ *(superseded by the line above; kept for the diagnostics)* The handshake fix is in `main` and unreleased. Two things to tell him beyond "try this": at the read tier he is now offered 36 tools rather than 66 (the new filter working as designed — and relevant because **ChatGPT restricts write-capable connectors to Business/Enterprise/Edu**, so on Plus or Pro his connector is read-only whatever scope he approved); and if it still shows nothing, **Connections → Connection details & health → Client traffic** is the next diagnostic. A `tools/list` row with status 200 and a non-zero count means Saddle is conformant and the tools are being dropped inside ChatGPT — a [documented](https://community.openai.com/t/mcp-connected-but-not-invokable/1384847) connector bug. **Still ask him to rotate the mrr.org.uk credentials he emailed in plaintext.** |
| 311 | +- **Mark Roach — reply drafted, awaiting Fahim's send. Two retractions in it.** (1) The MCP-Adapter diagnosis was WRONG: his own report says `Transport: Saddle built-in JSON-RPC`, so he does not have that plugin and rc6's headline fix is not his fix. (2) The trace shows ChatGPT's client (`Python/3.12 aiohttp/3.13.5`) refused **401 at the transport gate**, twice, with no `initialize` and no `tools/list` in the window — so this is authentication, full stop, not discovery, schemas, tiers or the handshake. Ask him for the **one** thing that closes it: run the Bearer curl probe below, then re-read Client traffic. rc7 now shows `auth:` and `scheme:` on every row, so the answer is readable without another round trip. |
| 312 | + ```bash |
| 313 | + curl -sS -o /dev/null -w '%{http_code}\n' -X POST https://staging.kesuk.net/wp-json/saddle/v1/mcp \ |
| 314 | + -H 'Content-Type: application/json' -H 'Authorization: Bearer saddle-probe-not-a-real-token' \ |
| 315 | + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' |
| 316 | + ``` |
| 317 | + `scheme:bearer` on that row → the header arrives, ChatGPT is not sending its token. `scheme:none` → 20i is stripping Bearer, and Connections → Connection details & health will now say so itself and offer the one-click rule. **Still ask him to rotate the mrr.org.uk credentials he emailed in plaintext.** |
| 318 | +- **rc7 is LIVE on the self-hosted channel** — `/v1/update` returns 1.0.0-rc7 with a signed package, and the zip served through the live link is byte-identical to the local build (sha256 `8ea73d41…`, 651,504 bytes) and **contains the updater** (verified by unzipping what the URL returns, not what was uploaded). First rc to actually reach anyone since rc5. |
| 319 | +- **Confirm which zip Mark installed.** `class-saddle-updater.php` ships only in `-selfhosted`; if he has the `.org` zip his site never checks for updates and rc7 will not reach him either. One more manual install and then never again. |
| 320 | +- **The bearer probe is stubbed, not field-proven.** `test_self_check_catches_a_bearer_stripped_while_basic_survives` drives a loopback that answers differently per scheme. Mark's site is the live test. If his report says `scheme:bearer`, the probe is correct and the problem is OpenAI's side of the token exchange — record that here either way. |
301 | 321 | - **Migrate saddle-pro and mailyard onto `saddle_context_sections`.** Free's half shipped in #102 and both still append through the old string filter, so Pro's `##` heading and Mailyard's floating "For email delivery problems:" sentence are still in the live context. Pro's tree currently has uncommitted release prep in it (a 1.4.0 → 1.4.1 bump), which is why it was left alone. Nothing breaks meanwhile — the compat path is tested. |
302 | 322 | - **The three surviving branches are all superseded or dead — decide and delete.** `feat/admin-header` (9 commits) and `feat/ui-plugpress-ui-v0.6.0` (a strict subset of it) targeted `@plugpress/ui` v0.8.2; `main` is on **v0.11.3**, TopBar and `Saddle_Log::record_action()` are both there, and every file they add exists on main in restructured form. `feat/freemius-parent` puts Freemius in FREE, which contradicts both CLAUDE.md ("no licensing or upsell in free") and the decision that free ships on WP.org with no Freemius. |
303 | 323 | - **Delete the v1.1.0 GitHub release + tag (one command):** `gh release delete v1.1.0 --repo plugpressco/saddle --cleanup-tag --yes && git tag -d v1.1.0`. Still outstanding, and #69 tried to reintroduce 1.1.0 this session — reverted in four places, including a `Stable tag` that was not a conflict and would have shipped silently. |
|
0 commit comments