diff --git a/CHANGELOG.md b/CHANGELOG.md index e7c7cf8..0a0ace2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 4a14ee7..5b6d0b8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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": ""}`. 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. diff --git a/README.md b/README.md index a10983f..0af7a08 100644 --- a/README.md +++ b/README.md @@ -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": ""}`. 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 @@ -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 diff --git a/src/mcp_clipboard/instructions/clipboard_version.md b/src/mcp_clipboard/instructions/clipboard_version.md new file mode 100644 index 0000000..5172560 --- /dev/null +++ b/src/mcp_clipboard/instructions/clipboard_version.md @@ -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. diff --git a/src/mcp_clipboard/instructions/server.md b/src/mcp_clipboard/instructions/server.md index f8dfa47..d69e158 100644 --- a/src/mcp_clipboard/instructions/server.md +++ b/src/mcp_clipboard/instructions/server.md @@ -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. diff --git a/src/mcp_clipboard/server.py b/src/mcp_clipboard/server.py index 6e57a5a..8fb9714 100644 --- a/src/mcp_clipboard/server.py +++ b/src/mcp_clipboard/server.py @@ -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. diff --git a/tests/test_server.py b/tests/test_server.py index 8cb47e1..41ea045 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -50,6 +50,7 @@ clipboard_list_formats, clipboard_paste, clipboard_read_raw, + clipboard_version, ) # --------------------------------------------------------------------------- @@ -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()