Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ _Notes on upcoming releases will be added here_

The MCP install widget on the front page, {doc}`/quickstart`, and {ref}`clients` now lets you pick a config scope per LLM client alongside the install method — *local* / *user* / *project* for Claude Code, *user* / *project* for Codex and Gemini, *project* / *global* for Cursor. JSON-only clients (Cursor, Claude Desktop) also show the destination config-file path next to the snippet so the paste target is never a guess. Your scope choice is remembered per-client, so switching between clients restores each one's last selection.

**Installer picker can apply or bypass dependency cooldowns**

A new *Configure cooldowns* control on the install widget lets you tack a cooldown onto the snippet without leaving the page. Pick a delay in days (uv's `--exclude-newer`, pip's `--uploaded-prior-to`, pipx's `--pip-args`) to wait out community vetting before pulling a fresh release, or pick *Bypass any global cooldown* to skip a `~/.config/uv/uv.toml` cutoff and grab the latest libtmux-mcp anyway. The setting persists across pages, and the embedded *What are cooldowns?* expander links to the [Datadog Security Labs writeup](https://securitylabs.datadoghq.com/articles/dependency-cooldowns/) and [cooldowns.dev](https://cooldowns.dev/) if you want the supply-chain context. (#31)

## libtmux-mcp 0.1.0a7 (2026-05-16)

### Breaking changes
Expand Down
11 changes: 9 additions & 2 deletions docs/_ext/widgets/_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
from __future__ import annotations

import pathlib
import shutil
import typing as t

from sphinx.util import logging
from sphinx.util.fileutil import copy_asset_file

from ._base import BaseWidget

Expand All @@ -28,6 +28,13 @@ def install_widget_assets(
every page includes them (same pattern as ``sphinx-copybutton``). This is
intentionally simpler than per-page inclusion — the files are small and the
docs are not bandwidth-constrained.

Uses :func:`shutil.copy2` directly. Recent Sphinx releases tightened
``copy_asset_file`` to refuse overwriting (it emits a
``misc.copy_overwrite`` warning and aborts), which leaves stale widget
JS/CSS on every incremental rebuild. The fix is to do the byte copy
ourselves: the cache-busting ``?v=<hash>`` querystring on the
``add_*_file`` registration line keeps browser caches honest.
"""
if app.builder.format != "html":
return
Expand All @@ -47,5 +54,5 @@ def install_widget_assets(
if not source.is_file():
continue
dest.mkdir(parents=True, exist_ok=True)
copy_asset_file(str(source), str(dest))
shutil.copy2(str(source), str(dest / filename))
register(f"{STATIC_SUBDIR}/{name}/{filename}")
54 changes: 54 additions & 0 deletions docs/_ext/widgets/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ class HighlightFilter(t.Protocol):
def __call__(self, code: str, language: str = "default") -> markupsafe.Markup: ...


class CooldownDaysSlotFilter(t.Protocol):
"""Callable signature for the Jinja ``cooldown_days_slot`` filter."""

def __call__(self, html: object) -> markupsafe.Markup: ...


class widget_container(nodes.container): # type: ignore[misc] # docutils nodes are untyped
"""Wraps a widget's rendered HTML; visit/depart emit the outer div."""

Expand Down Expand Up @@ -99,6 +105,7 @@ def render(
lstrip_blocks=True,
)
jenv.filters["highlight"] = make_highlight_filter(env)
jenv.filters["cooldown_days_slot"] = make_cooldown_days_slot_filter()
template = jenv.from_string(source)
context: dict[str, t.Any] = {
**cls.default_options,
Expand All @@ -109,6 +116,53 @@ def render(
return template.render(**context)


def make_cooldown_days_slot_filter() -> CooldownDaysSlotFilter:
"""Return a Jinja filter that injects cooldown slot ``<span>``s.

The Pygments highlighter escapes two days-mode sentinels emitted in
snippet bodies (see :mod:`docs._ext.widgets.mcp_install`):

* ``&lt;COOLDOWN_DURATION&gt;`` — used by uvx and pip days bodies.
Swapped for a span whose default text content is ``P<N>D`` (ISO
8601 duration). uv stores the value as
``ExcludeNewerValue::Relative(ExcludeNewerSpan)`` and recomputes
``now - N days`` on every resolver call; pip 26.1+ does the same
at flag-parse time per invocation. The snippet stays fresh
forever once saved to an MCP config.
* ``&lt;COOLDOWN_DATE&gt;`` — used by pipx days bodies because
pipx 1.8.0 bundles a pip older than 26.1 that rejects the
duration form with ``Invalid isoformat``. Swapped for a span
whose default text content is an absolute ISO date
(``today - default-days``). Drifts daily but ``widget.js``
refreshes the slot on every page load.

Both spans live inside a Pygments string-literal span and inherit
the parent's color. Their ``textContent`` is rewritten by
``widget.js`` whenever the user changes the days input. The filter
is a no-op for outputs without either sentinel (off and bypass
cooldown modes never emit one).
"""
from .mcp_install import DEFAULT_COOLDOWN_DAYS, default_cooldown_date

default_date = default_cooldown_date(DEFAULT_COOLDOWN_DAYS)
duration_span = (
'<span class="lm-mcp-install__cooldown-days"'
f" data-cooldown-duration-slot>P{DEFAULT_COOLDOWN_DAYS}D</span>"
)
date_span = (
'<span class="lm-mcp-install__cooldown-days"'
f" data-cooldown-date-slot>{default_date}</span>"
)

def _filter(html: object) -> markupsafe.Markup:
s = str(html)
s = s.replace("&lt;COOLDOWN_DURATION&gt;", duration_span)
s = s.replace("&lt;COOLDOWN_DATE&gt;", date_span)
return markupsafe.Markup(s)

return _filter


def make_highlight_filter(env: BuildEnvironment) -> HighlightFilter:
r"""Return a Jinja filter that runs Sphinx's Pygments highlighter.

Expand Down
Loading