# io.github.ostapondo/plonk (npm · plonk-mcp)

MCP server for Plonk: macOS window zones, workspaces, keep-awake, screenshots and on-device OCR.

- Trust score: 71/100 (medium)
- Change this week: +4
- Registry status: active
- Liveness: live
- Owner verified: no
- Last scored: 2026-08-17

## Components

- npm · `plonk-mcp`: 71/100 (this document), [markdown](https://verifymcp.io/servers/ostapondo-plonk/plonk-mcp.md), [page](https://verifymcp.io/servers/ostapondo-plonk/plonk-mcp)

## Channel facts

- Registry: `npm`
- Package: `plonk-mcp`
- Version: `0.2.5`
- Transport: `stdio`

## Trust breakdown

How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. Scores are 0–100 per category. Scoring method: https://verifymcp.io/docs/scoring (what has changed: https://verifymcp.io/docs/scoring/changelog)

Scored 2026-08-17.

- **Supply Chain Security**: 98/100
  - No malware found by supply-chain analysis.
  - No known CVEs affecting this package version or its production dependencies.
  - No install/post-install scripts declared.
  - 30 of 96 dependencies flagged as unhealthy.
- **Provenance & Transparency**: 48/100
  - Source repository is publicly reachable at the declared URL.
  - Build provenance is cryptographically sound, but it attests a different repository to the one declared in the registry. Most often the declared URL is simply stale.
  - Clear OSI-approved license (MIT).
  - Actively maintained (last published 1 days ago).
  - Publishes a security disclosure policy (SECURITY.md).
- **Schema Quality & AI Usability**: 66/100
  - AI-judged instruction clarity (excellent).
  - Context-footprint check failed: tool/resource definitions use about 3500 tokens (~184/item across 19 items; 19 tools + 0 resources), over budget; trim descriptions and params.
  - Usage-examples check failed: none of the tools include examples.
- **Stability & Change Management**: 30/100
  - Stability observed for 9 of 30 days with no destabilising changes; credit accrues until the full window elapses.
- **Tool Coverage**: 91/100
  - 100% of tools have a non-trivial description (not blank, and not just the tool's name).
  - 74% of tool parameters carry a description.
- **Capabilities**: 100/100
  - Implements a supported MCP spec version (2025-11-25); the latest is 2026-07-28.

## Install

### Claude

```bash
claude mcp add ostapondo-plonk -- npx -y plonk-mcp
```

### Codex

```bash
codex mcp add ostapondo-plonk -- npx -y plonk-mcp
```

### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "ostapondo-plonk": {
      "type": "local",
      "command": [
        "npx",
        "-y",
        "plonk-mcp"
      ],
      "enabled": true
    }
  }
}
```

### OpenClaw

```bash
openclaw mcp add ostapondo-plonk --command npx --arg -y --arg plonk-mcp
```

### Hermes

```yaml
mcp_servers:
  ostapondo-plonk:
    command: "npx"
    args: ["-y", "plonk-mcp"]
```

### Other

```json
{
  "mcpServers": {
    "ostapondo-plonk": {
      "command": "npx",
      "args": [
        "-y",
        "plonk-mcp"
      ]
    }
  }
}
```

## Changelog

Every change recorded for this component, newest first. Days that predate change tracking, or that we cannot explain, say so: "we were watching and nothing happened" and "we were not watching" are different claims.

### 2026-08-17 (score 71, +1)

No change was recorded against any check on this day. Stability & Change Management went from 27 to 30. That category is still filling its 30-day observation window: 8 days of observed history at the previous scan, 9 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-08-15 (score 70, +1)

- [security regression] Stability: 0.20 → unverified
- [functional regression] Capabilities: pass → unverified
- [functional regression] Tool coverage: 100 → unverified
- [functional] Package version: 0.2.4 → 0.2.5

### 2026-08-13 (score 69, +1)

No change was recorded against any check on this day. Stability & Change Management went from 13 to 17. That category is still filling its 30-day observation window: 4 days of observed history at the previous scan, 5 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-08-11 (score 68, +1)

- [functional] We updated how we score, so this day's move reflects our rubric, not a change to the server

### 2026-08-10 (score 67, +11)

- [security improvement] Known CVEs: unverified → pass
- [functional improvement] Dependency health: unverified → 0.85
- [functional improvement] Security disclosure: fail → pass
- [functional] Package version: 0.2.3 → 0.2.4

### 2026-08-09 (score 56, +1)

- [functional improvement] Stability: unverified → 0.03
- [functional] Package version: 0.2.2 → 0.2.3

### 2026-08-08 (score 55, +27)

- [security regression] Provenance: fail → unverified
- [security improvement] Known CVEs: unverified → pass
- [security] Stability: Stability not yet verified: not enough scan history yet (needs a 30-day window).
- [functional improvement] Tool coverage: unverified → 100
- [functional improvement] MCP protocol: unverified → pass
- [functional improvement] Dependency health: unverified → 0.85
- [functional] First check of Tool coverage: 73
- [functional] First check of Schema quality: fail
- [functional] First check of Schema quality: excellent
- [functional] First check of Schema quality: fail
- [functional] First check of Tool coverage: 68
- [functional] Package version: 0.0.3 → 0.2.2
- [functional] Package version: 0.0.3 → 0.2.0
- [functional] Package version: 0.0.3 → 0.1.0
- [functional] Package version: 0.0.3 → 0.0.4

### 2026-08-07 (score 28)

First indexed and scored.

## MCP tools (19)

### `get_state` (~114 tokens)

Get the current desktop state: all screens/monitors (index, frame, visible area — coordinates have origin at top-left of the primary screen, y grows down), all open windows (app name, title, which screen it is on, absolute frame, and 'fraction' — its position as fractions 0..1 of that screen's visible area), saved layout names, and whether keep-awake is on. ALWAYS call this first before applying a layout, to see which apps are running and how many monitors there are.

### `save_workspace` (~144 tokens)

Save a workspace: the apps of a desktop setup, where each window goes, and what each app should open. Pass 'items' to describe the arrangement, or omit them to snapshot the windows exactly as they are on screen right now. Saving over an existing name replaces it. Saved workspaces are listed in get_state, with their full contents.

Input parameters:

- `items` (array)
- `move_existing` (boolean): When true (the default), an app that is already running has its windows moved into place. When false, running apps are left alone and only missing apps are launched.
- `name` (string, required): Workspace name, e.g. 'work', 'writing'

### `launch_workspace` (~147 tokens)

Launch a saved workspace: opens every app that is not running, waits for its windows, and moves them into the saved positions. Each window returns to the monitor it was captured on, so a workspace spanning several displays comes back spanning them. macOS cannot open an app straight into a position, so windows appear first and jump into place. Returns per-app success, and reports apps that never opened a window. Takes up to a minute for a large workspace.

Input parameters:

- `name` (string, required)
- `screen` (integer): Pull the whole workspace onto this monitor instead of the ones it was captured on. Use when a display is no longer attached, or to move a setup to another screen.

### `delete_workspace` (~37 tokens)

Delete a saved workspace by name. Use this to clean up workspaces you created that are no longer wanted.

Input parameters:

- `name` (string, required)

### `apply_layout` (~210 tokens)

Move and resize windows to build a layout. Each item places one window: 'app' is the app name (fuzzy matched), optional 'title' filters windows of that app by title substring, optional 'screen' is the monitor index from get_state (each monitor can get its own layout — just send items with different 'screen' values; defaults to the screen the window is currently on), 'frame' is {x,y,w,h} as fractions 0..1 of that screen's visible area with origin at TOP-LEFT (left half = {x:0,y:0,w:0.5,h:1}; bottom-right quarter = {x:0.5,y:0.5,w:0.5,h:0.5}; centered 60% = {x:0.2,y:0.15,w:0.6,h:0.7}). Windows are unminimized if needed. Returns per-item success/errors.

Input parameters:

- `items` (array, required)

### `save_layout` (~118 tokens)

Save the named window arrangement as a workspace. Legacy name kept for older clients — new integrations should call save_workspace, which can also record whether running apps get moved into place. Omit 'items' to snapshot the windows exactly as they are on screen right now; pass 'items' to describe the arrangement explicitly. Saving over an existing name replaces it. Saved workspaces are listed in get_state, with their full contents.

Input parameters:

- `items` (array)
- `name` (string, required): Workspace name, e.g. 'work', 'focus'

### `apply_saved_layout` (~117 tokens)

Launch a saved workspace by name. Legacy name kept for older clients — new integrations should call launch_workspace, which adds a 'screen' option to pull the whole workspace onto one monitor. Opens every app that is not running, waits for its windows, and moves them into the saved positions; macOS cannot open an app straight into a position, so windows appear first and jump into place. Returns per-app success and reports apps that never opened a window. Takes up to a minute for a large workspace.

Input parameters:

- `name` (string, required)

### `snap_window` (~154 tokens)

Drop one window into a numbered zone of the snap-zone set assigned to that monitor. The numbers are the ones Plonk draws on the zones while a window is dragged, so 'the middle zone' of a three-zone set is 2. Zone sets and their per-monitor assignment are in get_state; use apply_layout instead when the user describes a size rather than a zone.

Input parameters:

- `app` (string, required): App name to match, e.g. 'Visual Studio Code'
- `screen` (integer): Monitor index; defaults to the one the window is on
- `title` (string): Only windows whose title contains this substring
- `zone` (integer, required): 1-based zone number, as shown on the drag overlay

### `delete_layout` (~75 tokens)

Delete the saved workspace with that name, whether it was saved with save_layout or save_workspace. Legacy name kept for older clients — new integrations should call delete_workspace, which does the same. Use it to clean up saved workspaces that are no longer wanted; existing names are listed in get_state.

Input parameters:

- `name` (string, required)

### `save_zone_set` (~153 tokens)

Create or replace a named zone set used for drag snapping. Zones are rectangles {x,y,w,h} as fractions 0..1 of a screen's visible area, origin TOP-LEFT; each zone must stay inside the screen, but zones may overlap each other (the smallest one under the cursor wins). Pass 'screen' to also assign the set to that monitor so it becomes active immediately. Built-in sets already exist: Halves, Thirds, 60 / 40, Quarters, Priority.

Input parameters:

- `name` (string, required): Zone set name, e.g. 'coding'
- `screen` (integer): Monitor index to assign this set to (0 = primary)
- `zones` (array, required)

### `assign_zone_set` (~133 tokens)

Assign a zone set (built-in or saved) to one monitor, so dragging a window there snaps to that set's zones. Each monitor keeps its own assignment; assigning replaces whatever that monitor used before and takes effect on the next drag. Omit 'name' to restore the default set (Halves); pass 'edge' for plain edge snapping instead of zones. Available set names and current per-monitor assignments are in get_state.

Input parameters:

- `name` (string): Zone set name, or 'edge' for edge snapping; omit for the default set
- `screen` (integer, required): Monitor index (0 = primary)

### `delete_zone_set` (~138 tokens)

Delete a saved zone set by name. Any monitor currently using it falls back to the default set (Halves), so snapping keeps working. Only sets made with save_zone_set can go: the built-ins (Halves, Thirds, 60 / 40, Quarters, Priority) are refused. Deleting is immediate and cannot be undone — the zones would have to be described again. Saved sets and their per-monitor assignments are listed in get_state; use assign_zone_set instead when a monitor should merely stop using a set that others still need.

Input parameters:

- `name` (string, required): Saved zone set name, as shown in get_state

### `set_awake` (~327 tokens)

Turn keep-awake on or off, so the Mac does not sleep part-way through something. Three ways to end the session, in order of preference: 'pid' ends it the moment that process exits — best by far when something is running, because a build or a render knows when it is finished and nothing is left holding the machine awake afterwards; 'until' ends it at a wall-clock time ('17:00', or an ISO-8601 timestamp); 'minutes' ends it after a countdown. Give none of them and it runs until switched off. Behavior also follows the user's settings: keep-awake may pause on battery or engage automatically while charging, so the returned 'status' is what actually happened and 'awake' is whether an assertion is held right now. The menu bar icon glows while it is. A process-bound session is deliberately not restored if Plonk restarts, since the pid would mean nothing by then. Errors come back for a pid that is not running or a time that has already passed.

Input parameters:

- `minutes` (integer): End the session after this many minutes
- `on` (boolean, required)
- `pid` (integer): End when this process exits. Use the pid of the long job being waited on; get_state lists a pid for every open window
- `until` (string): End at a time of day, e.g. '17:00' (the next such moment — tomorrow if today's has passed), or an ISO-8601 timestamp like '2026-08-08T17:00:00Z'

### `take_screenshot` (~375 tokens)

Capture the screen and return the image so it can be looked at. mode 'screen' captures everything (no user interaction); mode 'app' captures one named window and needs no user interaction either — pass 'app' and/or 'title_contains', and it works even when that window is behind others, minimized excepted, without raising it or taking focus; 'region' and 'window' hand the user the native crosshair/window picker and wait for them. Prefer 'app' whenever the user asks about a particular program ("what is playing in Spotify", "read the error in Xcode") — it is the only mode that can see a window the user cannot, and it does not disturb their desktop. Set annotate=true to open Plonk's drawing editor on the capture instead of returning it — use that when the user wants to mark the shot up themselves. Optional 'path' writes to an explicit file, otherwise the configured screenshot folder is used; 'clipboard' overrides the configured copy-to-clipboard behavior. The returned image is scaled down for legibility; the file at 'path' keeps full resolution. To draw on the result, pass that 'path' to annotate_screenshot.

Input parameters:

- `annotate` (boolean): Open the annotation editor instead of returning the image
- `app` (string): mode 'app': app name or bundle id, case-insensitive substring (e.g. 'Spotify')
- `clipboard` (boolean): Also copy the capture to the clipboard
- `include_image` (boolean): Return the image content itself, so it can be inspected (default true)
- `mode` (string)
- `path` (string): Explicit output file path (.png)
- `title_contains` (string): mode 'app': narrows to a window whose title contains this, for an app with several

### `annotate_screenshot` (~201 tokens)

Draw on a screenshot you already took, then copy it to the clipboard and show it to the user. Call take_screenshot first and LOOK at the image: you cannot know where anything is until you have seen it. Points are fractions 0..1 of the image, origin TOP-LEFT, so a rectangle around a left sidebar that is a seventh of the width and starts under the title bar is [{x:0,y:0.05},{x:0.14,y:1}]. Rectangle and ellipse take two opposite corners, arrow takes start then tip, pen and highlight take a run of points. Returns the marked image so you can check what you drew.

Input parameters:

- `clipboard` (boolean): Copy the result to the clipboard (default true)
- `marks` (array, required)
- `output` (string): Where to write it; defaults to the source name plus ' marked'
- `path` (string, required): Path returned by take_screenshot

### `extract_text` (~437 tokens)

Read the words off the screen, or off a saved image, and return them as text. Recognition runs on the Mac itself and nothing is uploaded. Prefer this over take_screenshot whenever the answer is words rather than a picture — an error dialog, a log, a terminal, a table, text baked into an image or a paused video, a PDF page in a viewer that will not let text be selected. It costs a fraction of the tokens an image does and does not depend on reading pixels correctly. Use take_screenshot instead when layout, colour or 'what does this look like' is the question. mode 'screen' captures everything with no user interaction; 'region' and 'window' hand the user the native crosshair or window picker and wait for them, up to five minutes. Pass 'path' instead of a mode to read an image already on disk, including one take_screenshot just wrote. Returns 'text' (every line in reading order, top to bottom) and 'lines' — each with the recognized string, Vision's 0..1 'confidence', and 'box' {x,y,w,h} as fractions 0..1 of the image with origin at TOP-LEFT. Those boxes share the coordinate space annotate_screenshot draws in, so a line can be circled where it was found by passing the same path to that tool. The text is also copied to the clipboard unless 'clipboard' is false. An area with no readable text returns ok with an empty 'text' and a 'note' rather than an error.

Input parameters:

- `clipboard` (boolean): Copy the recognized text to the clipboard (default true)
- `languages` (array): BCP-47 tags to recognize, most likely first, e.g. ['uk-UA','en-US']. Omit to use the user's configured choice. Which are available depends on the macOS version; get_state lists the current setting un…
- `mode` (string): What to capture; ignored when 'path' is given
- `path` (string): Read this image file instead of capturing (.png, .jpg)

### `select_agent` (~147 tokens)

Make an agent the user's active one in Plonk. Omit 'name' to select this client itself; pass "" to clear the choice so any agent may drive. The active agent shows in Plonk's menu bar and settings, and is where voice and other outgoing requests will go. With 'exclusive' true the app also rejects window and settings changes from every other agent (they can still read state and take screenshots). Connected agents are listed in get_state under 'agents'.

Input parameters:

- `exclusive` (boolean): Also turn 'only the active agent controls' on or off
- `name` (string): Agent name from get_state's 'agents'; omit for this client, "" to clear

### `check_for_update` (~239 tokens)

Ask Plonk whether a newer release exists, and report what is installed. Use this when the user asks what version they run, whether Plonk is up to date, or before calling install_update — which refuses unless a newer release is already on offer. The check is a network round trip to the GitHub releases API, so this returns immediately with the state as it stands and the result lands a moment later: read it back from get_state's 'update' key, or wait for an 'update' event on the change stream. If the user has turned update checks off, this fails with 409 rather than dialling out on their behalf — Plonk promises a process that only listens, and the user can still check by hand on its Updates page; report that back instead of retrying. Returns {installed, latest?, available, phase, status, automatic, notes?, page?}: 'available' is true only when 'latest' is newer than 'installed', 'phase' is idle|checking|available|downloading|verifying|installing|failed, and 'status' is a sentence fit to show the user.

### `install_update` (~234 tokens)

Install the release that check_for_update found: Plonk downloads the build, checks it is signed with the same certificate as the running copy, swaps the bundle in, and relaunches itself. Prefer this over telling the user to download a build by hand — the signature check is also what preserves their Accessibility and Screen Recording grants, which a hand-installed copy can lose. Ask the user before calling it: it quits the app, so any window arrangement in flight stops and the local API is unreachable for a few seconds until the new copy is up. It fails without touching the installed copy when no newer release is on offer (call check_for_update first), when the user has update checks switched off (409 — installing downloads a build, so it is bound by the same promise as the check; they can install from Plonk's Updates page), when the download does not match the release or its signature, or when Plonk.app sits somewhere the user cannot write. Returns the same shape as check_for_update plus {installing: true} once the swap has started; poll get_state afterwards to confirm the new version came up.

## Diagnostics

Captured diagnostic sections: Provenance, Dependencies. The full working is on the page: https://verifymcp.io/servers/ostapondo-plonk/plonk-mcp#diagnostics

## Score history

- 2026-08-17: 71
- 2026-08-16: 70
- 2026-08-15: 70
- 2026-08-14: 69
- 2026-08-13: 69
- 2026-08-12: 68
- 2026-08-11: 68
- 2026-08-10: 67
- 2026-08-09: 56
- 2026-08-08: 55
- 2026-08-07: 28

## Links

- npm package: https://www.npmjs.com/package/plonk-mcp
- Socket report: https://socket.dev/npm/package/plonk-mcp
- Repository: https://github.com/ostapondo/plonk
- Website: https://ostapondo.github.io/Plonk/
- Changelog RSS feed: https://verifymcp.io/servers/ostapondo-plonk/plonk-mcp.xml
- Changelog JSON feed: https://verifymcp.io/servers/ostapondo-plonk/plonk-mcp.json
- HTML version of this page: https://verifymcp.io/servers/ostapondo-plonk/plonk-mcp
