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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project will be documented here.

## [Unreleased]

### Added
- New `clipboard_version` MCP tool that returns the running
mcp-clipboard package version. Diagnostic surface for hosts and
agents that don't otherwise expose `serverInfo` to the model
(notably Claude Desktop on Windows). Test harnesses can now
record `mcp_clipboard_version` from inside an MCP session
without depending on a shell or filesystem access.

## [2.6.1] - 2026-05-07

### Fixed
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,14 @@ git tag test-v0.1.x && git push origin test-v0.1.x # triggers test-publish.yml

Three-layer design with clean separation:

- **server.py** — MCP server (`FastMCP`, name `mcp_clipboard`) exposing 6 tools:
- **server.py** — MCP server (`FastMCP`, name `mcp_clipboard`) exposing 7 tools:
- `clipboard_paste(output_format, include_schema, selection)` — Primary tool. Handles any clipboard content: tables → markdown/json/csv; non-tabular → smart formatting (JSON, code, URL, text). Images are returned as base64-encoded image content. Audio/video are detected and reported. `selection` defaults to `"clipboard"`; pass `"primary"` on X11/Wayland to read the middle-click/select-text-to-paste buffer.
- `clipboard_copy(content)` — Writes text to the system clipboard.
- `clipboard_copy_markdown(text)` — Renders markdown to HTML and places both formats on the clipboard so paste targets pick the right one. macOS/Windows write both atomically via multi-format clipboard APIs; Wayland/X11 are single-MIME and write only `text/html`.
- `clipboard_copy_image(image_data, mime_type)` — Writes a PNG or JPEG image to the system clipboard from base64-encoded bytes. Pass-through, no re-encoding. Magic bytes are validated against the declared MIME.
- `clipboard_read_raw(mime_type, selection)` — Returns raw clipboard content for a given MIME type (truncated at 50KB). Rejects binary MIME types. Accepts `selection="primary"` on X11/Wayland.
- `clipboard_list_formats(selection)` — Lists available MIME types on clipboard. Accepts `selection="primary"` on X11/Wayland.
- `clipboard_version()` — Returns the running mcp-clipboard package version as `{"name": "mcp-clipboard", "version": "<x.y.z>"}`. Diagnostic surface for hosts that don't expose the standard MCP `serverInfo` block to the model, and for test harnesses that need to record which build served a given run.

- **clipboard.py** — Platform-agnostic clipboard abstraction. Auto-detects backend (Wayland `wl-paste`/`wl-copy`, X11 `xclip`, macOS `osascript`/`pbpaste`/`pbcopy`, Windows PowerShell). All operations are async with 5-second timeout. Exit code 1 means "format not available" (not an error). macOS UTI types and Windows format names are mapped to MIME types in `list_formats`. Supports text read/write and binary image reads.

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Linux only. macOS and Windows have no equivalent buffer; passing `selection="pri
| `clipboard_copy_image` | Write a PNG or JPEG image to the system clipboard from base64-encoded bytes. Pass-through with no re-encoding; magic bytes are validated against the declared MIME. Use `clipboard_copy` for text. |
| `clipboard_list_formats` | List what MIME types are currently on the clipboard. Accepts `selection="primary"` for the X11/Wayland PRIMARY selection. |
| `clipboard_read_raw` | Return raw clipboard content for a given MIME type (diagnostic). Any non-binary type passes through; only `image/*`, `audio/*`, `video/*`, and `application/octet-stream` are rejected. Use `clipboard_paste` for images. Accepts `selection="primary"` for the X11/Wayland PRIMARY selection. |
| `clipboard_version` | Return the running mcp-clipboard package version as `{"name": "mcp-clipboard", "version": "<x.y.z>"}`. Diagnostic. Useful for hosts that don't surface the standard MCP `serverInfo` block to the model, and for test harnesses that need to record which build served a given run. |

## Setup

Expand Down Expand Up @@ -392,7 +393,8 @@ mcp-clipboard/
│ │ ├── clipboard_copy_markdown.md
│ │ ├── clipboard_paste.md
│ │ ├── clipboard_read_raw.md
│ │ └── clipboard_list_formats.md
│ │ ├── clipboard_list_formats.md
│ │ └── clipboard_version.md
│ └── icons/ # SVG icons for MCP client display (light/dark)
│ ├── mcp-clipboard-logo-light.svg
│ └── mcp-clipboard-logo-dark.svg
Expand Down
13 changes: 13 additions & 0 deletions src/mcp_clipboard/instructions/clipboard_version.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Return the version of mcp-clipboard currently running.

Use this when you need to record or report which mcp-clipboard build is
serving the current MCP session — for example, in test harnesses,
diagnostic dumps, or when the user asks which version is installed. The
version is read from the package metadata at runtime, so it always
matches the wheel actually serving the call.

Returns:
A dict with keys "name" (always "mcp-clipboard") and "version" (the
installed version string, e.g. "2.6.1"). If the package metadata
cannot be located (uninstalled source checkout), returns
"0.0.0+dev" as the version.
5 changes: 5 additions & 0 deletions src/mcp_clipboard/instructions/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,8 @@ is set due to a single-MIME-per-call limit (Wayland auto-advertises a
`text/plain` target whose bytes are the rendered HTML, not the markdown
source); for a plain-text paste of the markdown source on Linux, call
`clipboard_copy` with the markdown source directly.

Use `clipboard_version` to record or report which build of mcp-clipboard is
serving the current MCP session. Diagnostic only; useful when the user asks
which version is installed, or when a test harness needs to capture the
running version into a result entry.
15 changes: 15 additions & 0 deletions src/mcp_clipboard/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,21 @@ async def clipboard_copy_markdown(text: str) -> str:
)


@mcp.tool(
name="clipboard_version",
description=_load_instruction("clipboard_version"),
annotations={ # type: ignore[arg-type]
"title": "Clipboard Server Version",
"readOnlyHint": True,
"destructiveHint": False,
"idempotentHint": True,
"openWorldHint": False,
},
)
async def clipboard_version() -> dict[str, str]:
return {"name": "mcp-clipboard", "version": __version__}


_HELP_TEXT = """\
mcp-clipboard: MCP server for reading and writing the system clipboard.

Expand Down
23 changes: 23 additions & 0 deletions tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
clipboard_list_formats,
clipboard_paste,
clipboard_read_raw,
clipboard_version,
)

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -4302,3 +4303,25 @@ def test_cli_check_dispatches_through_main(monkeypatch):
# _run_check returned 0; main() called sys.exit(0).
assert exc_info.value.code == 0
mock_run.assert_not_called()


# ---------------------------------------------------------------------------
# clipboard_version
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_clipboard_version_returns_package_version():
"""clipboard_version returns the live package __version__."""
from mcp_clipboard import __version__

result = await clipboard_version()
assert result == {"name": "mcp-clipboard", "version": __version__}


def test_load_instruction_clipboard_version():
"""The clipboard_version instruction file is shipped and loadable."""
result = _load_instruction("clipboard_version")
assert isinstance(result, str)
assert len(result) > 0
assert "version" in result.lower()