Skip to content

feat(clipboard): X11 PRIMARY / Wayland primary-selection support on read tools#122

Merged
cmeans-claude-dev[bot] merged 1 commit into
mainfrom
feat/primary-selection-110
May 6, 2026
Merged

feat(clipboard): X11 PRIMARY / Wayland primary-selection support on read tools#122
cmeans-claude-dev[bot] merged 1 commit into
mainfrom
feat/primary-selection-110

Conversation

@cmeans-claude-dev
Copy link
Copy Markdown
Contributor

@cmeans-claude-dev cmeans-claude-dev Bot commented May 6, 2026

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 selection parameter:

  • 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

Backend Mechanism
Wayland wl-paste --primary (and --list-types --primary) — the _wayland_primary_args(selection) helper emits the flag
X11 xclip -selection primary — the existing -selection arg already accepted both values
macOS _reject_non_clipboard_selection("macOS") raises ClipboardError for any non-default selection. No silent fallback to clipboard — that would mask host-model intent mismatches
Windows Same as macOS

Validation is layered:

  • Backend-level (_validate_selection) raises ClipboardError so internal callers (other backends, future write paths) get a typed failure.
  • Tool-surface (_validate_tool_selection) returns a friendly string the host model sees, before the read is attempted.
  • macOS and Windows reject "primary" explicitly rather than silently falling back to "clipboard".

Verification

  • 26 new unit tests in tests/test_server.py:
    • Per-backend selection handling: Wayland adds --primary; X11 swaps the -selection arg; macOS/Windows raise ClipboardError at all three read entry points (read, list_formats, read_image).
    • Public-API threading: read_clipboard, list_clipboard_formats, read_clipboard_image pass selection to the dispatch.
    • Tool-surface validation: invalid value returns "Invalid selection" string; case-normalized; threading from clipboard_paste through to read_clipboard calls.
    • Backend-level rejection of unknown values (e.g., "secondary").
  • 2 new Xvfb integration tests in tests/test_integration_x11.py:
    • test_x11_primary_selection_round_trip — write to PRIMARY directly via xclip -selection primary, read back via read_clipboard(selection="primary"). Verifies CLIPBOARD remains independent (writing PRIMARY doesn't leak into CLIPBOARD).
    • test_x11_primary_list_formatslist_clipboard_formats(selection="primary") reports primary's targets.
  • Existing 573 unit tests updated where they patched dispatch dicts: mock signatures now accept selection="clipboard"; assertions on read_clipboard / read_clipboard_image / list_clipboard_formats mocks expect the second positional argument. No test deletions.
  • Full suite: 599 passed, 19 deselected, 5 xfailed (573 prior + 26 new).
  • ruff check, ruff format --check, mypy src/: all clean.

Out of scope (intentional, per #110)

  • Write-side PRIMARY support — the issue noted "lower priority" for symmetric write to PRIMARY. Deferring keeps the diff focused; a follow-up can wire selection into clipboard_copy and the writer dispatch.
  • SECONDARY selection on X11 — almost no app uses it; not worth the surface area.

Test plan

  • CI green across lint, typecheck, test (3.11/3.12/3.13), integration-x11, version-sync, validate-server-json.
  • X11 round trip on real xclip: select text in a terminal (no Ctrl-C), call clipboard_paste(selection="primary"), returns the selected text. Default clipboard_paste() returns the prior CLIPBOARD content unchanged.
  • Wayland round trip with wl-paste --primary available.
  • macOS round trip when maintainer has access: clipboard_paste(selection="primary") returns the "macOS does not support selection='primary'" error.
  • Windows round trip when access is available: same expected error.

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

…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>
@github-actions github-actions Bot added the Awaiting CI Dev complete, waiting for CI/Codecov to pass before QA label May 6, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions github-actions Bot added Ready for QA Dev work complete — QA can begin review and removed Awaiting CI Dev complete, waiting for CI/Codecov to pass before QA labels May 6, 2026
Copy link
Copy Markdown
Owner

@cmeans cmeans left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@cmeans cmeans added the QA Active QA is actively reviewing; Dev should not push changes label May 6, 2026
@github-actions github-actions Bot removed the Ready for QA Dev work complete — QA can begin review label May 6, 2026
Copy link
Copy Markdown
Owner

@cmeans cmeans left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_selection raises typed ClipboardError (for internal callers and future write paths); tool-layer _validate_tool_selection returns a friendly string the host model sees. Both layers gate selection. The two frozenset({"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_pasteselection is 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_formats Xvfb tests passing in CI
  • Wayland round trip — verified live on be8ae39 with 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_SELECTIONS and tool-surface _VALID_SELECTIONS_TOOL are 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.

@cmeans
Copy link
Copy Markdown
Owner

cmeans commented May 6, 2026

QA Audit — PR #122 / round 1

  • Branch: feat/primary-selection-110 @ be8ae39
  • Verification re-run locally: pytest 599 passed / 19 deselected / 5 xfailed; ruff check src+tests+scripts + format + mypy clean; new test counts match (26 unit + 2 integration).
  • Live Wayland verification covers all the happy-path and error-path branches:
    • Default reads CLIPBOARD ✓
    • selection="primary" reads PRIMARY ✓
    • Case-insensitive selection="PRIMARY" works (lowercase normalization) ✓
    • Invalid selection="secondary" returns "Invalid selection: 'secondary'. Supported: clipboard, primary."
    • CLIPBOARD/PRIMARY independence verified — read PRIMARY doesn't disturb CLIPBOARD ✓
    • clipboard_read_raw(selection="primary") and clipboard_list_formats(selection="primary") both work ✓
  • Layered validation correctly fires at the tool surface (friendly string for the host model) before any backend call.
  • 3 of 5 test-plan checkboxes ticked (CI + Wayland + X11); macOS + Windows deferred per CLAUDE.md.
  • No findings; no follow-up tickets.

Applying Ready for QA Signoff as the final act.

@cmeans cmeans added Ready for QA Signoff QA passed — ready for maintainer final review and merge QA Approved Manual QA testing completed and passed and removed QA Active QA is actively reviewing; Dev should not push changes Ready for QA Signoff QA passed — ready for maintainer final review and merge labels May 6, 2026
@cmeans-claude-dev cmeans-claude-dev Bot merged commit edabf71 into main May 6, 2026
35 checks passed
@cmeans-claude-dev cmeans-claude-dev Bot deleted the feat/primary-selection-110 branch May 6, 2026 03:54
@cmeans-claude-dev cmeans-claude-dev Bot mentioned this pull request May 6, 2026
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>
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>
@cmeans cmeans mentioned this pull request May 8, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

QA Approved Manual QA testing completed and passed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

X11/Wayland PRIMARY selection support

1 participant