feat(web): mureo configure Web UI (Phase 1) + official Google Ads provider usability fix#99
Merged
Conversation
Brings C3-C9 web work (handlers, server, oauth_bridge, legacy_commands, configure_cmd, _data/web/* SPA assets) onto this branch. The external process committed C1-C9 to feat/core-providers-capabilities by mistake. C1-C2 were already on this branch under different SHAs (same content, verified); C3-C9 are now consolidated here as a single commit. P1-01 changes (mureo/core/, tests/core/) which were mixed in by the misplaced branch are intentionally excluded — they live on feat/p1-01-capabilities (PR #90).
Auto-fixes from ruff --fix --unsafe-fixes: - TC003: move pathlib.Path imports into TYPE_CHECKING blocks - UP035: use collections.abc.Callable instead of typing.Callable - black reformatting on 4 files Behavior unchanged; existing 69 tests still pass.
Recreate 9 test files lost in the 2026-05-14 branch contamination disaster (FIX-12). The implementation in mureo/web/ survived; only tests were lost. 268 new tests across: - test_web_no_framework.py (19) — AGENTS.md L191 AST scan - test_web_csrf.py (44) — session.py CSRF, compare_digest - test_web_security_headers.py (44) — _helpers.py CSP/XFO/nosniff - test_web_handlers.py (40) — route dispatch, Host validation - test_web_host_paths.py (18) — Claude Code vs Desktop paths - test_web_server.py (25) — ConfigureWizard lifecycle - test_web_oauth_bridge.py (25) — WebAuthWizard spawn (mocked) - test_web_legacy_commands.py (20) — legacy slash command cleanup - test_web_static_serving.py (33) — path traversal defense Coverage on mureo/web/: 0% -> 81.91% (meets CI --cov-fail-under=80). All tests pytest.mark.unit, no real network/subprocess/os.environ.
BUG-1 (discovered by test-writer during test restoration): every method in handlers.py referenced self.wizard but only self.server.wizard was set in server.py. Every real HTTP request would crash with AttributeError on the first wizard access. Fix: override BaseHTTPRequestHandler.setup() to alias self.wizard = self.server.wizard before super().setup(). BaseRequestHandler's __init__ -> setup() -> handle() lifecycle guarantees this runs before any do_GET / do_POST dispatch, so all 16 self.wizard.* call sites are healed by the 4-line addition. Validated end-to-end by 268 new tests in test_web_handlers.py and test_web_static_serving.py driving real HTTP round-trips against a real ConfigureWizard on 127.0.0.1:0. The test-writer's _wire_wizard_onto_handler autouse workaround has been removed.
Four cohesive sub-features for the configure Web UI (Phase 1, #88): 1. Uninstall feature - NEW mureo/cli/settings_remove.py: remove_mcp_config, remove_credential_guard. Atomic, idempotent, surgical pops only. New module to avoid worsening auth_setup.py size. - setup_cmd.remove_skills: bundle-driven allow-list, not glob. - setup_actions: remove_mureo_mcp/auth_hook/workflow_skills plus clear_all_setup orchestrator. Never touches credentials.json. - handlers: 4 new CSRF/Host-gated POST routes. - Dashboard per-row delete + bulk clear with double-confirm. 2. Legacy --web removal, net -568 LOC - Drop the auth setup --web flag and multi-platform browser wizard: render_home/after_platform/finish_confirm/done and run_web_wizard. WebAuthWizard class kept, oauth_bridge spawns it. OAuth completion now redirects to /done. 3. UX fixes from Founder hands-on testing - Next button right-aligned on all steps via CSS flex fix. - mureo native benefit text on provider-choice step. - .btn classes applied to all new buttons; bulk-clear red. - SC re-auth skipped when google_ads OAuth already on disk. - Top-right close button removed; legacy cross-platform CTA gone. 4. Env panel and locale - status_collector env_vars now read from credentials.json, was os.environ so wizard inputs never showed. Secret masking bullets plus last4, under 8 chars fully masked. Label is now mureo credentials. - oauth_bridge propagates session locale; web_auth inline i18n. Tests added: settings_remove 28, setup_cmd_remove 14, setup_actions_remove 29, status_collector 17. Web suite GREEN. Pre-commit review verdict: ready_for_user_approval, 0 CRITICAL/HIGH.
Bring the operational demo and byod commands into the single Web UI surface so non-engineers do not drop to the CLI (Phase 1, #88). Backend (new modules, mirror setup_actions pattern): - mureo/web/demo_actions.py: list_demo_scenarios, init_demo with _validate_target. Frozen DemoListResult / DemoInitResult. - mureo/web/byod_actions.py: byod_status / byod_import / byod_remove / byod_clear. Frozen ByodResult. _validate_xlsx_path enforces absolute path, .xlsx/.xlsm only, realpath traversal defense, symlink-escape defeat, NUL/control-char rejection, isfile. - handlers.py: 6 thin routes. GET /api/demo/scenarios and GET /api/byod/status are Host-gated no-CSRF (matches existing GET pattern); 4 POST routes through existing Host+CSRF preflight. Frontend: - dashboard.js: Demo section (scenario dropdown + target input + create) and BYOD section (status table, file-path import, per-platform remove, double-confirm clear-all). Reuses .btn classes and MUREO.confirmAction / postJson. - app.html: 2 dashboard-section containers. i18n.json: 33 symmetric EN+JA keys. Local-first: byod uses a server-side file-PATH input, not browser multipart upload (stdlib http.server, single localhost user). Tests: demo_actions 34, byod_actions 51, handlers +27. demo_actions 99% / byod_actions 95% coverage. Full web suite 506 GREEN, no regression. Pre-commit review: ready_for_user_approval, 0 CRITICAL /HIGH; security path-validation traced and verified.
Founder UX feedback on the configure Web UI Dashboard: 1. Left sidebar navigation - the 7 dashboard sections regrouped into 4 nav panes (Setup / Demo / Imported Data / Danger Zone). Click or Enter/Space toggles visibility; aria-current on active; semantic nav+ul; default = Setup. Section internals unchanged. 2. Term fix - replaced bundle XLSX / Bundle XLSX everywhere with the Founder-approved term: JA 広告データファイル (Excel), EN Ad data file (Excel). Added a template-to-Excel helper line. 3. Scenario dropdown overflow - select constrained to pane width (max-width 100%, box-sizing, ellipsis on closed state). 4. Scenario titles i18n - frontend translation map keyed by scenario name (demo.scenario.*), JA titles for the 4 scenarios; falls back to API title for unknown scenarios. Python scenario files untouched (Web UI scope). Frontend-static only (no Python, no /api change, no security boundary). 506 web tests still GREEN. i18n EN/JA parity 134/134.
- .app-main widened to 1200px only when dashboard is active (:has([data-dashboard]:not([hidden]))); wizard/landing stay 800px. Sidebar 180->210px, content pane absorbs the extra width. - Scenario <option> now shows the localised title only; the hardcoded English sc.blurb (from Python scenario registry, out of Web UI scope) is no longer concatenated, so JA locale shows fully Japanese scenario names. Frontend-static only. 506 web tests GREEN.
Founder: pick demo target dir and BYOD Excel file via a native dialog instead of typing a path. Browsers cannot expose a server- side absolute path; since the configure UI is single-user localhost, the server pops a native OS picker. - NEW mureo/web/native_picker.py: PickResult (frozen), pick_directory / pick_file. Spawns sys.executable -c with a module-constant tkinter script; argv-only title/patterns, shell=False, timeout=300. Cross-platform (macOS + Windows). tkinter-missing / cancel / timeout / nonzero all map to a PickResult envelope; never raises, server never crashes. - handlers.py: POST /api/pick/directory + /api/pick/file, thin, via existing CSRF+Host preflight. - dashboard.js: Browse buttons (demo target, BYOD Excel path); ok fills input, cancel no-op, error toast + input stays editable as fallback. - BYOD nav + section heading renamed to "BYOD" (EN+JA). i18n dashboard.browse / picker_error added, parity preserved. Picker is convenience only: returned path still validated by _validate_target / _validate_xlsx_path (cross-platform os.path, accepts C:\\). Command injection proven impossible (fixed argv). Tests: native_picker 24, handlers +12. 540 web GREEN, 98% coverage. Review: ready_for_user_approval, 0 CRITICAL/HIGH.
Browse buttons did nothing on macOS: a Tk dialog spawned from the localhost server child process is not GUI-activated and stays behind Finder. Fix: OS-aware picker. - native_picker.py: darwin uses osascript (macOS builtin) with a CONSTANT AppleScript (choose folder / choose file). Client title/patterns are IGNORED on macOS = zero injection surface; argv fixed [osascript, -e, <const>], shell=False, timeout=300. User-cancel (-128 / "User canceled") maps to cancelled, not error. Non-darwin keeps the existing tkinter subprocess path unchanged. Missing osascript/tkinter still degrades to manual entry; never raises. No new deps. - Removed the "write files only" (skip_import) checkbox from the demo menu (Founder: not needed). Backend init_demo still accepts the param; UI always sends false. i18n key dropped, EN/JA parity preserved. Tests platform-branched (sys.platform monkeypatched); hostile title/pattern proven byte-identical in argv. 554 web GREEN, native_picker 99%. Review: ready_for_user_approval, 0 CRIT/HIGH.
- Header nav gets a persistent Dashboard link (nav.dashboard, EN/JA) wired to navigateToDashboard so the user can reach the dashboard from the wizard/landing at any time. - Re-run wizard button now shows ONLY on the Setup nav group; hidden on Demo / BYOD / Danger Zone (selectNavGroup toggles it). - Danger Zone had only the Clear All button with no context; added dashboard.clear_all_desc explaining it deletes all mureo config (MCP/hook/skills/providers/legacy) and that ~/.mureo/credentials.json is NOT touched. EN/JA symmetric. Frontend-static only. i18n JSON valid, EN/JA parity preserved, web tests GREEN.
The re-run wizard button stayed visible on Demo/BYOD/Danger
because .btn sets an explicit display, overriding the UA
[hidden]{display:none} rule. Toggle element.style.display
directly in selectNavGroup instead of the hidden attribute.
Buttons with long i18n labels wrapped to multiple lines and grew vertically. .btn lacked white-space:nowrap.
The demo scenario dropdown (and other JS-built dashboard content: basic-setup rows, providers, env panel, BYOD status) was rendered once at boot in the default locale and never rebuilt on language toggle, because setLocale only re-applies data-i18n attributes — nodes whose text is set via textContent=MUREO.t(...) were frozen. app.js already dispatched mureo:locale_changed; added a single document listener in dashboard.js that calls the existing renderAll() (guarded to no-op when the dashboard is absent/hidden). Idempotent: each render clears its container, so en->ja->en keeps exactly 4 scenario options, no listener leak. Verified live: boot en, toggle ja -> scenario options become JA (ハロー効果 / 隠れたチャンピオン / ...), toggle back -> English, repeat -> stable count 4. 554 web tests GREEN (no backend change).
Found via isolated-HOME round-trip test: selecting the Claude Desktop host did nothing — setup_actions hardcoded install_mcp_config(scope=global), always writing the Claude Code ~/.claude/settings.json; claude_desktop_config.json was never created. - NEW mureo/web/desktop_mcp.py: surgically writes/removes the mcpServers.mureo block in claude_desktop_config.json, reusing desktop_installer._load_config/_atomic_write_config/_backup_config. Path via get_host_paths (not the macOS-only _macos_config_path). Preserves all other mcpServers + unrelated keys; refuses corrupt JSON / symlink; never touches ~/.mureo/credentials.json. - setup_actions: 8 wrappers gain host="claude-code" (default = byte-for-byte unchanged Code behavior). Desktop branch: mcp -> desktop_mcp; auth_hook -> noop unsupported_on_desktop (Desktop config has no hooks surface, writes nothing, part not marked); skills host-agnostic. clear_all_setup symmetric for Desktop; credentials.json never touched (decision #3 holds). - handlers: 5 setup POST handlers forward session.host. Non-macOS Desktop path falls back to ~/.claude/settings.json (documented Phase-1 limitation; surgical merge preserves any co-resident Code config — no data loss). Tests: desktop_mcp 27, setup_actions_host 25, handlers +11. desktop_mcp 100% coverage. 614 web GREEN, Code back-compat byte-for-byte. Review: ready_for_user_approval, 0 CRIT/HIGH.
The Desktop mcpServers.mureo block was written as a single
combined string: {"command": "/abs/python -m mureo.mcp"}.
Claude Desktop / the MCP launcher treats command as the
executable and args as a separate array, so a space-joined
string is not a runnable executable — Claude Desktop would fail
to spawn the mureo MCP server. Tests had only asserted the
string was recorded verbatim, so the wrong shape passed review.
Now writes {"command": sys.executable, "args": ["-m",
"mureo.mcp"]}, matching the proven Claude Code _MCP_SERVER_CONFIG
schema. Verified on a real isolated-HOME claude_desktop_config
.json. install_desktop_mcp_block signature gains args:list[str];
surgical-merge / atomic / corrupt+symlink refusal unchanged.
Code-host path byte-for-byte unchanged; credentials.json never
touched. 614 web GREEN.
Bug (Founder Desktop test): selecting Claude Desktop + Meta
official MCP showed registration complete but dashboard kept
meta-ads-official as not-installed, and no Meta OAuth occurred.
Root cause: install_provider/remove_provider had no host param
and always wrote the official MCP into Claude Code\047s
~/.claude/settings.json via config_writer; the provider POST
handlers did not forward session.host (every other setup
handler does). status_collector is already host-aware, so on
Desktop it read claude_desktop_config.json (empty) -> shown as
not installed; Claude Desktop never got the entry. Same class
of bug already fixed for mureo MCP; provider catalog was missed.
- desktop_mcp.py: generic install/remove_desktop_server_block
(arbitrary server id + block, http or command/args). The
existing mureo block fns become server_id=mureo wrappers;
mcpServers.mureo bytes identical (regression-pinned). Catalog
MappingProxyType deep-unfrozen to plain JSON.
- setup_actions: install_provider/remove_provider gain
host/home. Code host = verbatim prior behavior. Desktop host
writes the catalog mcp_server_config block into
claude_desktop_config.json (pipx/npm subprocess still runs,
host-agnostic; hosted_http = block only). credentials.json
never touched. MUREO_DISABLE_* stays Code-only (Phase-2).
- handlers: 2 provider routes forward home/host.
- UI: hosted_http OAuth note (EN/JA) rendered once at wizard
buildChoiceCard + dashboard renderProvidersSection, clarifying
Meta official auth happens in Claude on first use (RFC 9728
MCP OAuth, client-initiated) — configure cannot/should not.
Desktop meta-ads-official block verified == {"type":"http",
"url":"https://mcp.facebook.com/ads"}. 665 web GREEN, Code path
byte-for-byte, mureo block byte-parity. Review: ready, 0 CRIT/HIGH.
Live bug (Founder): starting Claude Desktop after installing
meta-ads-official errored \047claude_desktop_config.json ... not a
valid MCP server configuration ... skipped: meta-ads-official\047.
Claude Desktop config is STDIO-only (command/args); the
{"type":"http","url":...} block is valid only for Claude Code\047s
settings.json (native remote MCP). The prior host-aware fix
wrote the Code-shaped http block verbatim into Desktop config.
Desktop needs the standard mcp-remote npx stdio<->HTTP bridge
(also performs the RFC 9728 OAuth handshake on first connect):
- setup_actions._desktop_block_for(spec): hosted_http ->
{"command":"npx","args":["-y","mcp-remote",<url>]} (url read
by key from the trusted catalog, discrete argv element, no
shell/join); pipx/npm -> spec block verbatim.
- _install_provider_desktop uses it; generic desktop_mcp writer
UNCHANGED (translation happens before it). Code host path
byte-for-byte unchanged (still native http block).
- UI: dashboard.provider_desktop_node_note (EN/JA) shown once
for hosted providers when host=claude-desktop, noting Node
(npx) is required. mcp-remote is a user-side runtime prereq,
not a mureo Python dependency (AGENTS.md L191 intact).
Desktop meta-ads-official now == {"command":"npx","args":
["-y","mcp-remote","https://mcp.facebook.com/ads"]}; Code ==
{"type":"http","url":"https://mcp.facebook.com/ads"}. 685 web
GREEN, credentials.json untouched. Review: ready, 0 CRIT/HIGH.
Selecting the OFFICIAL Google Ads MCP from `mureo configure` registered
the provider but left it unable to authenticate:
1. the wizard's Developer-Token + Google OAuth step only ran for the
mureo-NATIVE provider choice, so picking *official* skipped
credential collection entirely; and
2. the registered `mcpServers["google-ads-official"]` block carried no
`env`, while the upstream `google-ads-mcp` reads its config ONLY from
environment variables, never mureo's `~/.mureo/credentials.json`.
Net effect: "✓ registered" then "✗ Failed to connect".
Fix:
- `build_credentials_env()` — reverse of the per-var writer's closed
allow-list; materialises {GOOGLE_ADS_*: value} from credentials.json.
- `extra_env` on `add_provider_to_claude_settings` /
`add_provider_and_disable_in_mureo` via a shared `_build_desired_config`
(idempotent; merged in the same single atomic write as the
MUREO_DISABLE_* auto-disable).
- `install_provider` threads `credentials_path` and injects the env into
the registered block for both Claude Code and Claude Desktop;
hosted_http (Meta) is excluded — it authenticates via browser OAuth on
first connect.
- wizard: `auth` step now runs for native AND official Google Ads and is
ordered before `providers_install` so credentials exist when the block
is written.
- credential values are redacted from any `claude mcp add-json` failure
message before it can reach a log line.
This commit also lands the in-progress phase-1 host-aware work the fix
builds on (Claude Code `~/.claude.json` MCP registry path, pipx
`--python sys.executable` pin for the upstream KeyError crash, Desktop
hosted_http manual_required + mureo disable-env, per-platform
credentials.json removal endpoint, UI/i18n wiring).
Tests: 3133 passed; `ruff check mureo/` clean. CHANGELOG updated.
CI mypy flagged the pre-existing port = int(self.server.server_address[1]) (broad typeshed union); the TCPServer this handler runs under always has the (host, port) tuple. Narrowly scoped ignore, matching the file's existing convention.
The test computed `expected` outside the platform patch while `resolved` was inside it; `_claude_desktop_settings_path` is OS-dependent, so the assertion passed on macOS but failed on the Linux CI. Compute both sides under the same Darwin patch.
…switch, accurate UI The five concerns are interdependent within shared files (setup_actions / config_writer / handlers / i18n / dashboard) and dependency-coupled, so they ship as one coherent commit rather than non-green intermediate splits. 1. hosted_http (Meta) is never wired as a raw ~/.claude.json http entry (no OAuth DCR → "✗ Failed to connect") and mureo-native Meta is NOT auto-disabled (auto-disabling before the official path works strands the user). Code, `mureo providers add` CLI, and Desktop all return manual_required and point to Claude's account-level Connectors. `providers remove` self-heals a stale MUREO_DISABLE_<P>. 2. `mureo providers confirm <id>` + Web "finalize" button: once the connector is VERIFIED Connected (timeout-bounded `claude mcp list` parse via is_hosted_provider_connected), set MUREO_DISABLE_<P> so the model stops calling credential-less native tools. Never before verification (no stranding). Desktop → manual. 3. Dashboard "mureo integrations": drop GA4 (mureo has no native GA4 — official-only) and add Search Console as a status-only row driven by the shared Google OAuth (reflects the wizard's Google sign-in; no standalone remove — that stays on the Google Ads row). 4. Dashboard "Official MCP providers" reflects the real hosted-connector state via a separate lazily-fetched POST /api/providers/hosted-status (kept out of /api/status so it never slows every poll); hosted rows show ✓ when Connected and have no Remove button. 5. Host-accurate copy: Connectors steps differ for terminal vs Desktop; provider-choice note moved under the OFFICIAL option; host selector relabelled "Claude Code (CLI, Desktop app)" / "Claude Desktop app (Chat, Cowork)". EN+JA in sync. Tests: 284 change-set tests pass; ruff+black clean; i18n EN/JA parity. code-reviewer APPROVE on each increment. CHANGELOG updated.
- Japanese host labels now both use the fullwidth comma 、 (Claude Code(CLI、デスクトップアプリ)) for consistency. - The credential-guard hook has no surface on Claude Desktop (install_auth_hook → noop:unsupported_on_desktop). The basic-setup list in BOTH the wizard and the dashboard now appends "(not available on the Desktop app)" / "(デスクトップアプリでは利用できません)" to that row only when the chosen host is Claude Desktop — never for Claude Code, where the hook is installed normally. i18n EN/JA parity preserved; static-translation (no XSS); code-reviewer APPROVE. NOT pushed (per request). CHANGELOG updated.
The Japanese 'wizard.step_progress' value still contained the English
word 'Step' ('Step {current} / {total}'), so the progress indicator on
every wizard step — including the Setup complete screen — rendered
English in the JA locale. Now 'ステップ {current} / {total}'. EN
unchanged; placeholders preserved; EN/JA key parity intact.
code-reviewer APPROVE. Not pushed.
The auth wizard's GA4 step rendered service-account-path / project-id inputs and a Done button whose handler only called onAllDone() — the values were never saved. GOOGLE_APPLICATION_CREDENTIALS / GOOGLE_PROJECT_ID were therefore never written to credentials.json and ga4-official launched unauthenticated (same class as the earlier Google Ads bug). Done now POSTs each value through the allow-listed /api/credentials/env-var writer (ga4 section) BEFORE advancing; it only proceeds if every write succeeds, otherwise it shows a save-failed message and stays on the step. Slot inputs now carry the env-var name + a localized label key; saving/failure status + GA4 labels added EN+JA. Test: test_code_host_injects_ga4_env_from_credentials pins the end-to-end contract (ga4 creds → install injects them into the ga4-official block). Full suite 3158 passed; ruff+black clean; code-reviewer APPROVE. Not pushed.
The dashboard "Claude app / 設定先" field rendered the raw host id (`claude-desktop` / `claude-code`). It now maps to the same friendly labels as the wizard host selector (reusing the existing wizard.host.* i18n keys, no new keys): claude-desktop → "Claude Desktop app (Chat, Cowork)" / "Claudeデスクトップアプリ(Chat、Cowork)", claude-code → "Claude Code (CLI, Desktop app)" / "Claude Code(CLI、デスクトップアプリ)". Sets data-i18n so a locale switch re-resolves; unknown/empty host falls back to the raw value with data-i18n removed. code-reviewer APPROVE. Not pushed.
buildStepBody_completed built its title/description/platform-summary/ dashboard-button via build-time MUREO.t() with no data-i18n attribute. The locale switcher (app.js setLocale → applyTranslations) only re-resolves [data-i18n] nodes, so switching language after the completed screen rendered left those nodes frozen in the build-time locale (English) while every other data-i18n screen updated — the reported "Setup complete stays English". (pending_meta already had data-i18n and updated, confirming the diagnosis.) Added data-i18n to the title h2, desc p, each platform <li>, and the dashboard button (existing keys reused; none added). code-reviewer APPROVE. Not pushed.
Bundled (interdependent, on the unmerged web-config PR; shared files — setup_actions / CHANGELOG / i18n — make a finer split produce non-green intermediate commits): - Backfill: install_mureo_mcp (basic-setup AND dashboard path) now sets MUREO_DISABLE_<P> for already-registered pipx/npm official providers (google/ga4) so "official added first, mureo MCP later" no longer leaves native+official both active with no precedence. Pure file read; Meta deliberately excluded (no network probe on basic setup — Meta stays the explicit confirm / toggle path). Best-effort, idempotent, never invents a mureo block. - Web UI per-platform native↔official toggle: status.mureo_disable exposes per-platform state; POST /api/providers/native-toggle sets/unsets MUREO_DISABLE_<P>. Switching TO official is allowed only when the official path is in effect (pipx/npm registered, or Meta connector verified Connected) — refused with an actionable detail; switching back to native is always allowed (un-strand). Host-aware (Code + Desktop), EN+JA, restart-to-apply note. - _apply_disable_env: single host-dispatch/env-var-name helper shared by backfill and the toggle (removes duplicated logic). - /done OAuth-wizard page: was a hardcoded English constant ignoring session locale; now render_done_page(locale) via _t + _html_lang, done.* keys EN+JA. - UI polish: GA4 has no native tools → its toggle is not rendered; colored ✓ (green) / ✗ (red) status marks (own span so a locale re-translation can't wipe them); spacing between the toggle and the note above. Tests: 3180 passed; ruff+black clean; i18n EN/JA parity. code-reviewer APPROVE (no CRITICAL/HIGH/MEDIUM). CHANGELOG updated. Not pushed.
Two intertwined concerns (README/CHANGELOG span both; unmerged branch,
verify-after-commit checkpoint):
1) Corrects an earlier WRONG premise. A hosted HTTP MCP (Meta) IS a
valid Claude Code user-scope server (`claude mcp add --transport
http`, independent of / higher precedence than claude.ai
Connectors). The 401 "✗ Failed to connect" before OAuth is the
expected pre-auth state.
- setup_actions._install_provider_code hosted_http: REGISTER the
http entry (add_provider_to_claude_settings, extra_env={}), return
new status `auth_required`. NO native auto-disable (registration ≠
authenticated; no-strand preserved — disable only via
`providers confirm`/native-toggle once verified Connected).
- CLI `providers add` hosted_http: registers (not the disable
variant) + prints accurate `/mcp` Authenticate steps (Meta no-DCR
caveat: re-register with --client-id/--client-secret/
--callback-port, or use the account connector). dry-run writes
nothing.
- auth_wizards.js: Code hosted → Install → `auth_required` → show
`/mcp` steps; Desktop hosted unchanged (manual_required /
Connectors). i18n connector.code.* / hosted notes / pending_meta
rewritten (EN+JA). dashboard.js comment refreshed.
- Claude Desktop path byte-for-byte unchanged.
- Tests rewritten to pin the corrected behavior.
2) Docs: `mureo auth setup --web` removed → `mureo configure` is the
documented front door. README gains a `pip install` + `mureo
configure` quickstart; cli.md / authentication.md / getting-started
.md(.ja) / byod.md(.ja) / architecture.md updated. CHANGELOG adds
the corrective "Fixed — Meta hosted on Claude Code" entry (it
explicitly supersedes the earlier Connectors-only bullets) + a
"Docs" entry.
Tests: 3180 passed; ruff+black clean; i18n EN/JA parity; JS syntax OK.
code-reviewer APPROVE (no CRITICAL/HIGH). NOT pushed — awaiting your
real-environment /mcp verification before push/merge.
Full app.css redesign ('calm precision instrument'): refined token
system (spacing/radius/shadow scales), light + dark via
prefers-color-scheme, system fonts only (strict CSP, no web fonts),
crafted cards/buttons/pills/focus states, restrained motion +
prefers-reduced-motion. Every existing class / data-* / data-i18n
hook preserved (CSS-only, no app.html structure change beyond the
logo).
Header now shows the official mureo wordmark: bundle logo.png /
logo-dark.png into _data/web, allowlist them in handlers.py
(_STATIC_ALLOWLIST), swap by prefers-color-scheme. CSP img-src
'self' covers /static.
Test plan:
- [x] test_web_static_serving (logo.png/-dark allowlisted, served)
- [x] full suite 3182 passed; black/ruff clean; JS node -c
…register) + Google Ads scope guidance Real-environment verification proved Meta's hosted MCP (mcp.facebook.com/ads) has NO OAuth dynamic client registration: Claude Code /mcp auth fails with 'redirect_uris are not registered for this client'. Registering it as a user-scope server only creates an unauthenticatable entry. Claude Code path corrected: mureo no longer registers meta-ads-official locally. install_provider / providers add return manual_required (no ~/.claude.json write, no subprocess); the UI/CLI steer the user to add Meta as a Claude.ai account connector (claude.ai -> Settings -> Connectors -> Add custom connector -> https://mcp.facebook.com/ads). No-strand preserved (mureo-native Meta never auto-disabled; only via providers confirm once the connector is verified Connected). Desktop unchanged. Supersedes the earlier '/mcp register on Code' Unreleased narrative. - setup_actions._install_provider_code hosted_http -> manual_required - providers_cmd: _emit_hosted_auth_notice + _add_one connector copy - auth_wizards.js: hosted (Meta) short-circuits to connector card on BOTH hosts; wizard.js completed reminder copy - i18n connector.code.* + wizard.completed.pending_meta (EN/JA) - catalog.py meta-ads-official notes; CHANGELOG/README re-corrected Also: Google Ads OAuth scope guidance — the auth step + docs now explain that a reused refresh token MUST carry https://www.googleapis.com/auth/adwords or calls fail with ACCESS_TOKEN_SCOPE_INSUFFICIENT, with the official scope-reference URL (auth_wizards.js + i18n EN/JA + docs/authentication.md). Test plan: - [x] full suite 3182 passed; black/ruff clean; JS node -c; i18n parity - [x] tests/test_cli_providers, test_web_provider_host updated to the not-registered/manual_required contract - [ ] real-env: add Meta via claude.ai connector, /mcp verify mcp__claude_ai_MetaAds__*; re-auth Google with adwords scope
…ase1 # Conflicts: # CHANGELOG.md
hyoshi
added a commit
that referenced
this pull request
May 16, 2026
- pyproject 0.8.0 -> 0.9.0; Development Status 3 - Alpha -> 4 - Beta - mureo/__init__.py __version__ 0.7.1 -> 0.9.0 (sync; was stale) - .claude-plugin/plugin.json 0.8.0 -> 0.9.0 - CHANGELOG: [Unreleased] -> [0.9.0] - 2026-05-16, plus entries for the /learn restore (#105), configure UI visual refresh + official logo, and Google Ads OAuth-scope guidance (post-#99 merges) Highlights: mureo configure Web UI (Phase 1) + visual refresh & logo; mureo providers CLI; plugin->MCP tool exposure; Meta on Claude Code via the Claude.ai connector; /learn restored; BYOD/demo date-anchor fix. Test plan: - [x] full suite 3209 passed - [x] python -m build + twine check PASSED (0.9.0 wheel + sdist) - [x] plugin manifest version-match test green
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 1 of the
mureo configureWeb UI (Issue #88) plus themureo providersCLI (Issue #86), culminating in a fix for a blocking bug: the official Google Ads MCP provider registered but was unusable.The bug (primary deliverable of the latest commit)
Selecting the official Google Ads MCP from the Web UI registered the provider but it could never authenticate:
mcpServers["google-ads-official"]block had noenv, while upstreamgoogle-ads-mcpreads config only from environment variables (never~/.mureo/credentials.json).Net effect: "✓ registered" then "✗ Failed to connect".
Fix
build_credentials_env()— reverse of the closed env-var allow-list →{GOOGLE_ADS_*: value}fromcredentials.json.extra_envonadd_provider_to_claude_settings/add_provider_and_disable_in_mureovia a shared_build_desired_config(idempotent; same single atomic write as theMUREO_DISABLE_*auto-disable).install_providerthreadscredentials_pathand injects env into the registered block for Claude Code and Claude Desktop; Meta hosted_http correctly excluded (browser OAuth on first connect).authruns for native and official Google Ads, ordered beforeproviders_installso credentials exist when the block is written.claude mcp add-jsonfailure message before it can be logged.Also in this branch
In-progress Phase-1 host-aware work the fix builds on: Claude Code
~/.claude.jsonMCP registry path, pipx--python sys.executablepin (upstreamKeyError: 'Author'crash), Desktop hosted_httpmanual_required+ mureo disable-env, per-platformcredentials.jsonremoval endpoint, UI/i18n wiring.Test plan
pytest— 3133 passedruff check mureo/— cleanenv→ MCP connects