feat(clipboard): X11 PRIMARY / Wayland primary-selection support on read tools#122
Merged
Merged
Conversation
…ead tools The X11 PRIMARY selection (middle-click / select-text-to-paste buffer) and the analogous Wayland primary selection are now reachable as a separate read source. Power-user workflow: select text in a terminal, browser, or vim without Ctrl-C, then call clipboard_paste with selection='primary' to read it. This is a "model can't do this on its own" capability — there is no PRIMARY-selection access without OS-level clipboard tools. Tool surface (all three read tools): - clipboard_paste(output_format, include_schema, selection='clipboard') - clipboard_read_raw(mime_type, selection='clipboard') - clipboard_list_formats(selection='clipboard') Pass selection='primary' for the PRIMARY buffer; selection='clipboard' (default) is unchanged. Invalid values return a friendly error. Public-API changes (private to package consumers): - read_clipboard(mime_type, selection='clipboard') - list_clipboard_formats(selection='clipboard') - read_clipboard_image(mime_type, selection='clipboard') All 12 backend functions (_wayland_*, _x11_*, _macos_*, _windows_*) take the selection parameter. Wayland honors it via the _wayland_primary_args helper that emits ['--primary'] when needed; X11 honors it via the existing -selection arg (which already accepted 'primary' and 'clipboard'). macOS and Windows raise ClipboardError via _reject_non_clipboard_selection if anything other than 'clipboard' is passed — silent fallback would mask host-model intent mismatches. Validation lives in two places by design: - _validate_selection / _validate_tool_selection: backend-level + tool- surface check. The tool-surface check returns a friendly string the host model sees; the backend-level check raises ClipboardError so internal callers (other backends, future write paths) get a typed failure. - macOS/Windows reject 'primary' explicitly rather than silently using 'clipboard'. Tests: - tests/test_server.py: 26 new unit tests covering each backend's selection handling (Wayland --primary flag, X11 -selection arg, macOS/Windows rejection at all three read entry points), the public API threading, the tool-surface validation (case-insensitive, invalid values, success paths), and the threading from clipboard_paste through to the underlying read_clipboard calls. - tests/test_integration_x11.py: 2 new Xvfb integration tests — test_x11_primary_selection_round_trip (write to PRIMARY directly via xclip, read back via read_clipboard(selection='primary'), confirm CLIPBOARD is independent) and test_x11_primary_list_formats. - Existing 573 unit tests updated where they patched the dispatch dicts: mock signatures now take `selection='clipboard'` keyword and assertions on read_clipboard / read_clipboard_image / list_clipboard_formats expect the second positional argument. Out of scope (intentional, per #110): - Write-side PRIMARY support. The issue noted "lower priority" for symmetric write to PRIMARY; deferring it keeps the diff focused. A follow-up can wire `selection` into clipboard_copy + the writers. - SECONDARY selection on X11 (almost no app uses it; not worth surface). Verification: - uv run pytest: 599 passed (573 prior + 26 new), 19 deselected, 5 xfailed. - ruff check / format / mypy: all clean. - Patch coverage on the new code: see PR body. Closes #110. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
cmeans
reviewed
May 6, 2026
Owner
cmeans
left a comment
There was a problem hiding this comment.
QA Review — Round 1
Verdict: Pass — zero findings.
Clean, well-scoped feature. All three read tools gain selection, layered validation is good (backend raises typed ClipboardError, tool surface returns friendly model-facing string), macOS/Windows reject explicitly rather than silently falling back. Live Wayland verification confirms PRIMARY/CLIPBOARD independence end-to-end.
Issue #110 acceptance
| Bullet | Status |
|---|---|
X11 — select text without Ctrl-C, clipboard_paste(selection="primary") returns it |
Covered by Xvfb integration test test_x11_primary_selection_round_trip (passes in CI on this commit); live X11 not directly verified in this Wayland session |
Wayland — wl-paste --primary available |
Verified live below; wl-copy --primary "PRIMARY-content" then clipboard_paste(selection="primary") returns it; CLIPBOARD remains independent |
| macOS/Windows — clear "primary selection not supported" error | Unit-tested at all three read entry points (_macos_read, _macos_list_formats, _macos_read_image, plus the Windows trio). Error string: "macOS does not support selection='primary'; only the 'clipboard' selection exists on this platform." — clean, model-actionable |
| Existing tests still pass | 599 passed (was 573 + 26 new); zero deletions; the existing patches that mocked dispatch dicts cleanly accept the new selection="clipboard" default arg |
CHANGELOG [Unreleased] / ### Added entry |
Present, accurate, (#122) - closes #110 link |
Verification (run locally on feat/primary-selection-110 @ be8ae39)
| Check | Result |
|---|---|
uv run pytest -q |
599 passed / 19 deselected / 5 xfailed (matches PR body 573 prior + 26 new; deselected 17 prior + 2 new integration) |
uv run ruff check src tests scripts |
All checks passed |
uv run ruff format --check src tests scripts |
11 files already formatted |
uv run mypy src |
Success: no issues found in 4 source files |
New unit test count (grep -cE '^\\+(async def |def )test_' tests/test_server.py) |
26 (matches PR body) |
| New integration test count | 2 (matches PR body) |
Wayland live: default clipboard_paste() after write_clipboard("CLIPBOARD-content") |
returns 'CLIPBOARD-content' ✓ |
Wayland live: clipboard_paste(selection="primary") after wl-copy --primary "PRIMARY-content" |
returns 'PRIMARY-content' ✓ |
Case-insensitive: clipboard_paste(selection="PRIMARY") |
returns 'PRIMARY-content' ✓ (selection.strip().lower() normalization works) |
Invalid value: clipboard_paste(selection="secondary") |
returns "Invalid selection: 'secondary'. Supported: clipboard, primary." ✓ — tool-surface validation fires before any backend call |
CLIPBOARD/PRIMARY independence: clipboard_paste(selection="clipboard") after the selection="primary" read |
still returns 'CLIPBOARD-content' — buffers truly independent ✓ |
clipboard_read_raw(mime_type="text/plain", selection="primary") |
returns Content (16 chars):\n\nPRIMARY-content ✓ |
clipboard_list_formats(selection="primary") |
returns 6 formats, including text/plain ✓ |
Invalid value on clipboard_list_formats(selection="oops") |
same friendly error string ✓ |
CI on be8ae39 |
All green: lint, typecheck, test (3.11/3.12/3.13), integration-x11, codecov/patch (100% patch), version-sync, validate-server-json |
Design review
- Layered validation — backend
_validate_selectionraises typedClipboardError(for internal callers and future write paths); tool-layer_validate_tool_selectionreturns a friendly string the host model sees. Both layers gateselection. The twofrozenset({"clipboard", "primary"})definitions look duplicate but serve different purposes (backend layer could reasonably expose new selections without the tool layer auto-promoting them); not flagging as drift. - Reject-not-fallback on macOS/Windows — explicit rejection is the right call per the issue's open question #2; matches the precedent set in PR #116 ("don't paper over platform differences").
- Internal threading in
clipboard_paste—selectionis correctly passed through to all read calls (HTML, plain, RTF, format-listing, image read), so the multi-strategy paste doesn't accidentally mix CLIPBOARD and PRIMARY in one call.
Test-plan checkbox status (post-review)
- CI green across
lint,typecheck,test (3.11/3.12/3.13),integration-x11,version-sync,validate-server-json— verified - X11 round trip — covered by
test_x11_primary_selection_round_trip+test_x11_primary_list_formatsXvfb tests passing in CI - Wayland round trip — verified live on
be8ae39with PRIMARY/CLIPBOARD independence + case-insensitive + invalid-value paths - macOS round trip — deferred per CLAUDE.md
- Windows round trip — deferred per CLAUDE.md
Follow-up tickets
None for #110 itself. Two pre-existing items the PR (correctly) did not pick up:
- Write-side PRIMARY support (
clipboard_copy(selection="primary")) — issue #110 noted lower priority; reasonable to defer to a separate ticket if/when wanted. - Backend-level
_VALID_SELECTIONSand tool-surface_VALID_SELECTIONS_TOOLare currently identical content. If they ever drift apart by intent, that's a feature; if by accident, future-you will hit the layering. Worth keeping on the radar but not filing until there's a reason to widen one without the other.
Applying Ready for QA Signoff as the final act.
Owner
|
QA Audit — PR #122 / round 1
Applying Ready for QA Signoff as the final act. |
Merged
4 tasks
cmeans-claude-dev Bot
added a commit
that referenced
this pull request
May 6, 2026
Release v2.5.0. Aggregates the four PRs since v2.4.0 (2026-05-05). ## Scope ### Added - **#120 — register with the MCP Server registry** (closes #114). New `server.json` (root) carries the registry manifest; `scripts/sync-server-json.py` is the single-source-of-truth sync from `pyproject.toml`'s `[project].version` to `server.json`'s two version fields. New composite action `.github/actions/install-mcp-publisher` pins `mcp-publisher` to `v1.7.6+` for the post-2026-04-30 OIDC audience requirement. CI gains `version-sync` + `validate-server-json` jobs; `publish.yml` gains a release-time `validate-server-json` gate (now `needs:` of `publish-pypi`) and a new `publish-registry` job that runs after `publish-pypi`. **This release is the first to fire `publish-registry`, registering `io.github.cmeans/mcp-clipboard` for the first time.** Subsequent releases update the entry in place. - **#121 — `clipboard_copy_markdown` tool** (closes #109). Renders a markdown source to HTML via `markdown-it-py` (with raw HTML escaped, safe by construction) and writes both `text/html` (rendered) and `text/plain` (the markdown source) to the clipboard. macOS and Windows write both formats atomically (NSPasteboard / `DataObject`); Wayland and X11 are single-MIME-per-call and write `text/html` only — Wayland's `wl-copy` auto-advertises `text/plain` whose bytes are the rendered HTML markup, X11 has no `text/plain` target. Adds `markdown-it-py>=3.0` as a new runtime dependency (pure Python, ~250 KB, no native deps). - **#122 — PRIMARY-selection support on read tools** (closes #110). `clipboard_paste`, `clipboard_read_raw`, and `clipboard_list_formats` now accept an optional `selection` argument (`"clipboard"` default, `"primary"` for the X11 PRIMARY / Wayland primary middle-click selection). macOS and Windows have no PRIMARY analog and return a clear error if `"primary"` is passed. Public APIs `read_clipboard`, `list_clipboard_formats`, and `read_clipboard_image` gained the same `selection` parameter. ### Closed without engagement - **#119 — SafeSkill drive-by scanner promotion.** Closed as duplicate of an identical filing on cmeans/mcp-synology. Same auto-template hit both repos; the methodology has gaps (`Files Scanned: 0` on Python projects, then asks for a promotional badge). ## Verification - `uv run pytest -q`: **599 passed**, 19 deselected, 5 xfailed. - `uv run ruff check src/ tests/ scripts/`: clean. - `uv run mypy src/`: clean. - `python scripts/sync-server-json.py --check`: server.json in sync with pyproject.toml (2.5.0). - `uv build --wheel`: builds `dist/mcp_clipboard-2.5.0-py3-none-any.whl` successfully. - All four landing PRs were QA-approved on `main` with green CI before this aggregation. ## Release commit shape Three-file diff: `pyproject.toml` (2.4.0 → 2.5.0), `CHANGELOG.md` (header roll, no other content changes), and `server.json` (synced via `scripts/sync-server-json.py` per the new release flow introduced in #120). The `version-sync` CI gate verifies the sync is correct before merge. ## Tag plan After this PR merges, push `v2.5.0` tag to trigger `publish.yml`: 1. `validate-server-json` — schema check against the live registry. 2. `publish-pypi` — uploads `mcp_clipboard-2.5.0-py3-none-any.whl` + sdist to PyPI via OIDC trusted publisher. 3. `publish-registry` — registers `io.github.cmeans/mcp-clipboard` in the official MCP registry. **First-run.** 4. (no `github-release` job yet — that's a future-port from mcp-synology if/when it's worth the diff.) The `[Unreleased]` section is now empty, ready for the next cycle. ## Test plan - [x] CI green across `lint`, `typecheck`, `test (3.11/3.12/3.13)`, `integration-x11`, `version-sync`, `validate-server-json`. - [x] `uv build --wheel` succeeds locally on a clean checkout (verified pre-PR). - [ ] Tag push triggers `publish.yml` and the PyPI release lands. - [ ] `publish-registry` job lands green on the first run; `curl -s 'https://registry.modelcontextprotocol.io/v0/servers?search=mcp-clipboard'` returns a hit afterward. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: cmeans-claude-dev[bot] <272174644+cmeans-claude-dev[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
cmeans-claude-dev Bot
added a commit
that referenced
this pull request
May 6, 2026
Closes #123. Promotes the X11/Wayland PRIMARY-selection support that shipped in #122 from "three parameter descriptions in the tools table" to a featured `### Bonus` subsection under `## Why This Exists`, alongside the existing table-paste pitch and the fixes-copying-from-Claude-Code pitch. ## What landed `README.md`: - New `### Bonus: read your X11/Wayland selection without Ctrl-C` subsection between the existing Claude-Code copy-from-terminal Bonus section and the `## Tools` table. - Two short sentences explaining the CLIPBOARD vs PRIMARY split (selection buffer, middle-click paste, updates on highlight without an explicit Ctrl-C). - Names the three tools that accept `selection="primary"` (`clipboard_paste`, `clipboard_read_raw`, `clipboard_list_formats`). - Four concrete workflow callouts: terminal triage, vim/IDE visual selection, browser/PDF reading, two-buffer (CLIPBOARD + PRIMARY in the same conversation). - Closes with the Linux-only caveat and the macOS/Windows clear-error behavior so users on those platforms aren't surprised. `CHANGELOG.md`: `[Unreleased] / ### Changed` entry. ## Why this shape Issue #123 outlined two options: a single bullet in `### Reading your clipboard` (light touch), or a featured subsection under `## Why This Exists` with workflows. Picked the featured shape because PRIMARY is structurally distinct from the Ctrl-C clipboard (different buffer, different trigger semantics) and is worth equal billing with the other two "why" pitches. The tools-table parameter descriptions still cover the light-touch surface for parameter-level skimmers. ## Verification - Docs only; no Python touched. CI's `lint`, `typecheck`, `test (3.11/3.12/3.13)`, `integration-x11`, `version-sync`, and `validate-server-json` should all stay green / no-op. - Verified no em-dashes in the new content (per repo convention) and that prose uses American spellings. - Renders cleanly as Markdown locally (no broken backticks or unclosed code spans). ## Out of scope - Sweeping pre-existing em-dashes elsewhere in `README.md` and `CHANGELOG.md` (lines 86, 231, 233, 255, 271 in README; many older CHANGELOG entries). Cosmetic; can be a separate sweep if desired. - Adding the same workflow callouts to `### Tips for reliable triggering` or `### Reading your clipboard` examples. Issue #123 explicitly asked for one shape, not both. ## Test plan - [ ] README renders cleanly on PyPI and on GitHub (no broken Markdown, code spans, or list indentation). - [x] `## Tools` table still renders correctly immediately after the new subsection (no orphan blank line, no list bleed). - [x] CI green across `lint`, `typecheck`, `test (3.11/3.12/3.13)`, `integration-x11`, `version-sync`, `validate-server-json`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: cmeans-claude-dev[bot] <272174644+cmeans-claude-dev[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
Closes #110. The X11 PRIMARY selection (middle-click / select-text-to-paste buffer) and the analogous Wayland primary selection are now reachable as a separate read source. Power-user workflow: select text in a terminal, browser, or vim without Ctrl-C, then
clipboard_paste(selection="primary")returns it.This is a "model can't do this on its own" capability — there's no PRIMARY-selection access without OS-level clipboard tools.
Tool surface
All three read tools gain an optional
selectionparameter:clipboard_paste(output_format, include_schema, selection="clipboard")clipboard_read_raw(mime_type, selection="clipboard")clipboard_list_formats(selection="clipboard")selection="primary"reads the PRIMARY buffer;"clipboard"(default) is unchanged. Invalid values return a friendly tool-error string. Case-insensitive ("PRIMARY"works).Per-platform implementation
wl-paste --primary(and--list-types --primary) — the_wayland_primary_args(selection)helper emits the flagxclip -selection primary— the existing-selectionarg already accepted both values_reject_non_clipboard_selection("macOS")raisesClipboardErrorfor any non-default selection. No silent fallback to clipboard — that would mask host-model intent mismatchesValidation is layered:
_validate_selection) raisesClipboardErrorso internal callers (other backends, future write paths) get a typed failure._validate_tool_selection) returns a friendly string the host model sees, before the read is attempted."primary"explicitly rather than silently falling back to"clipboard".Verification
tests/test_server.py:--primary; X11 swaps the-selectionarg; macOS/Windows raiseClipboardErrorat all three read entry points (read, list_formats, read_image).read_clipboard,list_clipboard_formats,read_clipboard_imagepassselectionto the dispatch.clipboard_pastethrough toread_clipboardcalls."secondary").tests/test_integration_x11.py:test_x11_primary_selection_round_trip— write to PRIMARY directly viaxclip -selection primary, read back viaread_clipboard(selection="primary"). Verifies CLIPBOARD remains independent (writing PRIMARY doesn't leak into CLIPBOARD).test_x11_primary_list_formats—list_clipboard_formats(selection="primary")reports primary's targets.selection="clipboard"; assertions onread_clipboard/read_clipboard_image/list_clipboard_formatsmocks expect the second positional argument. No test deletions.ruff check,ruff format --check,mypy src/: all clean.Out of scope (intentional, per #110)
selectionintoclipboard_copyand the writer dispatch.Test plan
lint,typecheck,test (3.11/3.12/3.13),integration-x11,version-sync,validate-server-json.xclip: select text in a terminal (no Ctrl-C), callclipboard_paste(selection="primary"), returns the selected text. Defaultclipboard_paste()returns the prior CLIPBOARD content unchanged.wl-paste --primaryavailable.clipboard_paste(selection="primary")returns the "macOS does not support selection='primary'" error.Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com