diff --git a/CHANGELOG.md b/CHANGELOG.md index 0008a58..f36b5a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - **publish.yml: bump pinned `mcp-publisher` v1.5.0 → v1.7.6 to match the new registry OIDC audience** (#79) — the v0.5.1 release ran with `mcp-publisher v1.5.0` (the pin in `.github/actions/install-mcp-publisher/action.yml`); PyPI publish succeeded but the `publish-registry` job failed at GitHub OIDC login with `invalid audience: expected https://registry.modelcontextprotocol.io, got [mcp-registry]` (HTTP 401). Root cause: the registry deployed [`modelcontextprotocol/registry#1229`](https://github.com/modelcontextprotocol/registry/pull/1229) ("auth: bind GitHub OIDC token exchange to a per-deployment audience") in `v1.7.6` on 2026-04-30 — one day before our 2026-05-01 release. v1.5.0's `login github-oidc` flow sends audience `mcp-registry`; v1.7.6's flow sends audience `https://registry.modelcontextprotocol.io`, which is what the new registry server validates against. Bumped the action's `default` from `v1.5.0` to `v1.7.6` (and added an explanatory comment so the next bump prompt has the rationale at hand). Re-running the failed `publish-registry` job on the existing v0.5.1 tag won't pick up this fix because `actions/checkout@v6` resolves to the tag's commit; the next release tag will exercise the fix end-to-end. v0.5.1 itself is on PyPI as expected and is the install path users actually hit; the missed registry entry is purely directory metadata. - **Keyring exception handler narrows + logs root cause** (#80) — closes #38. `core/auth.py:147-148` previously caught a bare `except Exception:` with a flat `logger.debug("Keyring not available.")`, hiding every keyring failure mode behind one generic line: locked macOS keychain (operator-actionable: unlock the keychain), `keyring.errors.NoKeyringError` on a headless host (signals a config issue), `keyring.errors.InitError`, OS-level errors on the D-Bus reach path, and genuine library bugs. Operators running `mcp-synology check -v` saw "Keyring not available." with no clue whether the keychain was locked, the backend was missing, or the library blew up. Narrowed to two typed handlers: `except KeyringError as e` (the typed-error case — covers `KeyringLocked`, `NoKeyringError`, `InitError`, `PasswordSetError`, `PasswordDeleteError`, and any other `keyring.errors.*` class) and `except OSError as e` (D-Bus reach errors, permission failures on the OS keychain DB). Both log at DEBUG with `exc_info=True` so the actual exception type, message, and traceback land in the verbose-mode output. Genuine bugs are no longer caught — they propagate up so they can be triaged. The pre-keyring D-Bus socket pre-check (`auth.py:130-131`) was bumped from DEBUG to INFO and rewritten with three concrete remediations (`mcp-synology setup` from a real desktop session, `dbus-run-session` wrapper, or `SYNOLOGY_USERNAME` / `SYNOLOGY_PASSWORD` / `SYNOLOGY_DEVICE_ID` env vars to bypass keyring entirely) — this is the most common path on Linux services launched without a desktop session, and the previous DEBUG-only log meant operators running `mcp-synology check` (without `-v`) saw a generic "no credentials" error with no breadcrumb to the root cause. Updated `tests/core/test_auth.py::_no_keyring()` fixture to raise `keyring.errors.NoKeyringError` instead of bare `Exception` so existing tests exercise the production-shaped error path. New `TestKeyringErrorHandling` (3 cases): `KeyringError` logged with `exc_info` and message text at DEBUG, `OSError` logged separately, and a defense-in-depth case proving a keyring blow-up doesn't block credential resolution from config/env. Strengthened `TestDbusSocketMissing::test_dbus_not_set_when_socket_missing_on_linux` to assert the new INFO-level remediation hint contains the socket path AND all three remediation strings. 553 unit tests pass at 96.13% coverage. - **Background update-check task: log swallowed exceptions and bound the executor** (#81) — closes #39. Two related gaps in `server.py::SharedClientManager._bg_update_check()`. (1) The `(OSError, ValueError, KeyError)` handler used a bare `pass`, so a failed PyPI check (DNS down, malformed version string in `global.yaml`, state-file `KeyError`) silently exited the background coroutine with no breadcrumb anywhere — `SYNOLOGY_LOG_LEVEL=debug` showed nothing. Now logs at DEBUG with `exc_info=True` so the actual exception type, message, and traceback land in verbose output. Update check is best-effort, so we still don't propagate; the tool flow runs fine without an update notice. (2) The `loop.run_in_executor()` call was unbounded. The inner `urlopen(timeout=5)` covered the socket, but a thread stuck after `urlopen` returned (pathological YAML parsing on a malformed PyPI response, slow disk on `_save_global_state()`) would keep this coroutine alive forever and could delay session shutdown. Now wrapped in `asyncio.timeout(10)` (10s outer bound, generous margin over the 5s socket timeout); on timeout `_bg_update_check` catches `TimeoutError` separately, logs at DEBUG, and exits cleanly so the user-facing tool flow is never blocked. New `TestSharedClientManagerLifecycle::test_bg_update_check_timeout_logged_and_swallowed` patches `asyncio.timeout` to a 50ms window and `run_in_executor` to a 5s sleep, asserting the coroutine returns normally with the expected DEBUG log. Strengthened `test_bg_update_check_swallows_errors` to assert the new "Update check failed" DEBUG record carries `exc_info`. 554 unit tests pass at 96.13% coverage. +- **Bump Pygments 2.19.2 → 2.20.0 to clear ReDoS advisory [GHSA-5239-wwwm-4pmq](https://github.com/advisories/GHSA-5239-wwwm-4pmq)** (#82) — closes Dependabot alert #3. Pygments < 2.20.0 has an inefficient regex for GUID matching that can be triggered into ReDoS by crafted input; severity Low. We pull Pygments transitively via `pytest` 9.0.3 (used by the test suite — `pytest --color` syntax-highlights traceback output), so this is a dev-dep upgrade that doesn't affect runtime. Bumped via `uv lock --upgrade-package pygments`; only `uv.lock` changed (no `pyproject.toml` constraint adjustment needed because `pytest` doesn't pin Pygments to an upper bound). 554 unit tests still pass at 96.13% coverage on the bumped lockfile; ruff/mypy clean. ## 0.5.1 (2026-05-01) diff --git a/uv.lock b/uv.lock index 8161623..02dccb5 100644 --- a/uv.lock +++ b/uv.lock @@ -1024,11 +1024,11 @@ wheels = [ [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]]