# io.github.gaozhao1989/pyjab-mcp (pypi · pyjab-mcp)

Drive Java desktop apps from an AI agent, through the JVM's own accessibility tree

- Trust score: 86/100 (high trust)
- Change this week: +3
- Registry status: active
- Liveness: live
- Owner verified: no
- Last scored: 2026-11-05

## Components

- pypi · `pyjab-mcp`: 86/100 (this document), [markdown](https://verifymcp.io/servers/gaozhao1989-pyjab-mcp/pyjab-mcp.md), [page](https://verifymcp.io/servers/gaozhao1989-pyjab-mcp/pyjab-mcp)

## Channel facts

- Registry: `pypi`
- Package: `pyjab-mcp`
- Version: `0.3.1`
- 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-11-05.

- **Supply Chain Security**: 100/100
  - No malware found by supply-chain analysis.
  - No known CVEs affecting this package version or its production dependencies.
  - Runs setuptools.build_meta at install time, a recognised build step with no custom scripting around it.
  - 1 of 29 dependencies flagged as unhealthy.
- **Provenance & Transparency**: 97/100
  - Source repository is publicly reachable at the declared URL.
  - Cryptographically verified build provenance (signed, bound to gaozhao1989/pyjab-mcp).
  - Clear OSI-approved license (MIT).
  - Actively maintained (last published 23 days ago).
  - Disclosure check failed: no security disclosure policy was found in the source repository.
- **Schema Quality & AI Usability**: 62/100
  - AI-judged instruction clarity (excellent).
  - Context-footprint check failed: tool/resource definitions use about 5768 tokens (~360/item across 16 items; 16 tools + 0 resources), over budget; trim descriptions and params.
  - Usage-examples check failed: none of the tools include examples.
- **Stability & Change Management**: 70/100
  - Stability observed for 21 of 30 days with no destabilising changes; credit accrues until the full window elapses.
- **Tool Coverage**: 71/100
  - 100% of tools have a non-trivial description (not blank, and not just the tool's name).
  - 0% of tool parameters carry a description.
  - Structured output schemas are declared (94% of tools); any adoption earns full credit.
- **Tool Safety**: 100/100
  - No prompt-injection markers were found in the server instructions, tool names or descriptions we captured.
  - All 1 tool(s) whose name or description implies an irreversible operation declare an MCP destructiveHint annotation.
  - An AI judge read all 17 captured unit(s) of tool text and found none that tries to manipulate the model reading it.
- **Capabilities**: 100/100
  - Implements a current MCP spec version (2026-07-28).

## Install

### How do I install the io.github.gaozhao1989/pyjab-mcp server?

io.github.gaozhao1989/pyjab-mcp runs locally as a PyPI package, launched with uvx pyjab-mcp. Ready-made configuration for Claude, Cursor, VS Code, Codex and 5 more is on this page, copied from each client's own documentation.

### Claude

```bash
claude mcp add gaozhao1989-pyjab-mcp -- uvx pyjab-mcp
```

### Cursor

```json
{
  "mcpServers": {
    "gaozhao1989-pyjab-mcp": {
      "command": "uvx",
      "args": [
        "pyjab-mcp"
      ]
    }
  }
}
```

### VS Code

```json
{
  "servers": {
    "gaozhao1989-pyjab-mcp": {
      "command": "uvx",
      "args": [
        "pyjab-mcp"
      ]
    }
  }
}
```

### Codex

```bash
codex mcp add gaozhao1989-pyjab-mcp -- uvx pyjab-mcp
```

### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "gaozhao1989-pyjab-mcp": {
      "type": "local",
      "command": [
        "uvx",
        "pyjab-mcp"
      ],
      "enabled": true
    }
  }
}
```

### OpenClaw

```bash
openclaw mcp add gaozhao1989-pyjab-mcp --command uvx --arg pyjab-mcp
```

### Hermes

```yaml
mcp_servers:
  gaozhao1989-pyjab-mcp:
    command: "uvx"
    args: ["pyjab-mcp"]
```

### Netclaw

```json
{
  "McpServers": {
    "gaozhao1989-pyjab-mcp": {
      "Transport": "stdio",
      "Command": "uvx",
      "Arguments": [
        "pyjab-mcp"
      ]
    }
  }
}
```

### Vellum

```bash
assistant mcp add gaozhao1989-pyjab-mcp -t stdio -c uvx -a pyjab-mcp
```

### Other

```json
{
  "mcpServers": {
    "gaozhao1989-pyjab-mcp": {
      "command": "uvx",
      "args": [
        "pyjab-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-11-05 (score 86, +15)

- [security improvement] Malware scan: unverified → pass

### 2026-11-04 (score 71, +1)

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

### 2026-11-02 (score 70, +1)

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

### 2026-10-31 (score 69, −14)

- [security regression] Malware scan: pass → unverified

### 2026-10-29 (score 83, +16)

- [security improvement] Malware scan: unverified → pass

### 2026-10-27 (score 67, +1)

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

### 2026-10-26 (score 66, −15)

- [security regression] Malware scan: pass → unverified

### 2026-10-25 (score 81, +1)

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

## MCP tools (16)

### `java_diagnostics` (~262 tokens)

Report whether this machine can drive a Java application right now.

Use this first when anything fails, and after installing a JVM or enabling Java
Access Bridge. It reports the platform, the interpreter, the installed pyjab and
which of its public APIs are present, whether Java Access Bridge is enabled, where
the bridge DLL is, and the state of the bridge thread.

It is read-only and never writes to the environment: it does not enable Java Access
Bridge, because a diagnostics tool that changes what it is reporting on is worse
than none.

Args:
    load_bridge: When true, also start the bridge thread first and report whether it
        came up. This is the end-to-end check; it is off by default so that a mere
        report cannot load a DLL.

Returns:
    A multi-line report. A line that could not be measured says
    ``UNAVAILABLE (reason)`` and is never reported as zero or as "missing" --
    "nothing is there" and "I could not read" are different answers.

Errors:
    None for the environment itself: an unusable environment is what this tool
    reports, so it does not fail because of one.

Input parameters:

- `load_bridge` (boolean)

Output parameters:

- `result` (string)

### `java_list_windows` (~210 tokens)

List the Java windows Java Access Bridge can see on this machine.

Use this to find out whether the application is visible to JAB at all, and to get a
title or hwnd for java_attach_window. If the application is not in this list,
nothing else will work and the problem is the attachment or the environment, not a
locator -- run java_diagnostics next.

Only windows of Java applications are listed; this is not a general Windows window
list.

Returns:
    One line per window, with hwnd, pid and title. "No Java windows are visible" is
    a measurement and is reported as such, not as an empty string.

Errors:
    ``is_error`` with an actionable message when pyjab cannot answer. On pyjab 1.9.0
    the listing runs through the documented ``pyjab-inspect windows --json``,
    because the function behind it is not public API yet; the message says so when
    even that is unavailable.

Output parameters:

- `result` (string)

### `java_attach_window` (~296 tokens)

Bind to one Java window and return a session id for later calls.

Use this after java_list_windows, once you know which window to drive. Give either
an exact ``hwnd`` or a ``title``; a title may be an fnmatch pattern
(``"*Report*"``) exactly as pyjab matches it. Pass ``hwnd`` when several windows
share a title pattern.

Binding is also what arms Java Access Bridge for this server: pyjab loads the DLL,
hooks the events and registers the JAB prototypes on the thread doing the binding,
so the first attach is slower than the later ones.

This never modifies, restarts or injects into the application, and it does not own
the process: the application keeps running whatever happens to this session.

Args:
    title: Window title, matched with fnmatch. Ignored when ``hwnd`` is given.
    hwnd: Window handle from java_list_windows. Takes precedence over ``title``.

Returns:
    A session id, and the window it is bound to. Pass the session id to
    java_snapshot.

Errors:
    ``is_error`` when the window cannot be bound, naming what was tried. A window
    that is not a Java window, or a title that never appears, is the usual reason;
    java_list_windows shows what JAB can see.

Input parameters:

- `hwnd`
- `title` (string)

Output parameters:

- `result` (string)

### `java_snapshot` (~528 tokens)

The bounded tree view of an attached window, one line per element, with handles.

This is the tool to reach for first after attaching: it shows what is in the window,
what each element is called, and a handle (``[e:1a2b]``) for every element a later
call can name. Pass a handle to a tool that acts on elements; pass a session id
here.

The view is deliberately bounded, and it says so when it was cut short. A Java
window can hold thousands of nodes, and returning one puts hundreds of thousands of
tokens into the conversation, so read in rings: snapshot at depth 2, then snapshot
again if you need to see inside something.

Handles are positions, not references: nothing is held between calls. Each handle
records the element's role, name, position among siblings and bounds, and a later
call re-finds the element and checks those before doing anything. If the window
moved on -- a list reloaded, a dialog replaced a panel -- the handle fails with the
difference rather than acting on whatever is there now.

Args:
    session_id: From java_attach_window.
    depth: How many levels below the window to read. 2 is a good default; raise it
        only when the elements you want are not visible at 2.
    max_nodes: Hard cap on rendered elements. When it is reached the reply says so
        and names the way to narrow the next read.
    only_interactive: Drop pure layout scaffolding (panes, panels, viewports). Set
        it to false when you need the structure rather than the controls, or when
        an element you expect is missing -- filtering is the usual reason.
    fields: Extra per-element fields to print: any of states (the default), bounds,
        description, children, index, depth. Role and name are always printed,
        because they are what a handle checks itself against.

Returns:
    One line per element, indented by depth, plus a truncation notice when the view
    was cut short and a count of elements that have no handle.

Errors:
    ``is_error`` for an unknow…

Input parameters:

- `depth` (integer)
- `fields`
- `max_nodes` (integer)
- `only_interactive` (boolean)
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `java_find` (~445 tokens)

Search an attached window and get a handle for each match.

Use this when a snapshot is too large to read, or when you know what the control is
called and do not want to walk the tree to find it. A snapshot shows you the shape of
the window; this asks a question about it.

Give **exactly one** selector. `name` and `description` are exact matches -- for a
substring use `xpath` with `contains()`, which is pyjab's locator syntax:
\`//push button[@name=contains('Save')]`. `states` takes a comma-joined list such as
\`"enabled,showing"`.

Every match gets a handle, and a handle is checked before it is used: the locator
recorded here is resolved again later and compared against the role, name, position
and bounds seen now. If a match cannot be given a locator that finds it again, it is
listed **without** a handle rather than with one that might resolve to something else.

Args:
    session_id: From java_attach_window.
    name: Exact accessible name.
    role: Role, as pyjab spells it: "push button", "text", "check box", "table".
    description: Exact accessible description, for controls with no name.
    states: Comma-joined states, e.g. "enabled,showing".
    xpath: A pyjab locator, for substrings, unions and paths.
    max_results: Stop after this many matches.

Returns:
    One line per match with its handle and locator, or a measurement that nothing
    matched -- with the selector that found nothing in it, so the next attempt can be
    narrower rather than identical.

Errors:
    ``is_error`` for an unknown session id, for no selector or more than one, or when
    the window can no longer be read.

Input parameters:

- `description` (string)
- `max_results` (integer)
- `name` (string)
- `role` (string)
- `session_id` (string, required)
- `states` (string)
- `xpath` (string)

Output parameters:

- `result` (string)

### `java_get_element` (~214 tokens)

Everything known about one element, and whether it is still the same element.

Use this after java_snapshot or java_find to look at one control in detail, or to
confirm that a handle still points at what it did. The handle is resolved again and
compared against what was recorded when it was handed out; if anything differs --
role, name, position among siblings, bounds -- this reports the difference instead of
presenting the element as if nothing had changed.

Args:
    session_id: From java_attach_window.
    handle: An ``e:...`` handle from a snapshot or a find.

Returns:
    The element's role, name, description, states, bounds, index and depth, its
    locator, and the verification result.

Errors:
    ``is_error`` for an unknown handle (the message says to take a fresh snapshot),
    for a handle belonging to another session, or when the element has changed.

Input parameters:

- `handle` (string, required)
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `java_get_text` (~278 tokens)

Read the text of an element: a label, a text field, a table cell, a document.

Use this on a handle from a snapshot or a find. The same verification applies, so a
text field that has been replaced since the snapshot reports the change instead of
returning whatever is there now.

Long values are cut, and the cut is reported: the reply says how much of the whole
text it carries and what the whole text's length is, so a partial read is never
presented as the value.

Args:
    session_id: From java_attach_window.
    handle: An ``e:...`` handle from a snapshot or a find.
    start: Character to start at, 0-based.
    length: How many characters to return.

Returns:
    The slice of the element's text, with its length and whether it was cut.

Errors:
    ``is_error`` for an unknown handle, a handle from another session, an element
    that has changed, or an element with no text at all -- which is reported as such,
    because "this control has no text" and "this control is empty" are different
    answers.

Input parameters:

- `handle` (string, required)
- `length` (integer)
- `session_id` (string, required)
- `start` (integer)

Output parameters:

- `result` (string)

### `java_get_table` (~413 tokens)

Read a Java table, a page of rows at a time.

Use this on a handle for a `table` element -- from java_snapshot or java_find. This is
the tool a platform-level automation cannot replace: it reads the table's own
accessibility interface, so cell text, row and column counts come from the JVM rather
than from pixels or from a bridge that does not carry them.

It never returns the whole table. A large table is 200 rows in pyjab's own test
application and thousands in a real one, so the read is paged by `row_start` and
bounded by `row_count` and `max_cells`, and the reply says what it left out and how to
ask for it.

Cells are not just text. A cell with no text interface is shown as `-`, and a cell
that could not be read is shown as `?` with a count: "this cell has no text" and "I
could not read this cell" are different answers, and reporting the second as the
first would invent an empty table.

Args:
    session_id: From java_attach_window.
    handle: A handle for the table element.
    row_start: First row to read, 0-based.
    row_count: How many rows to read.
    max_cells: Hard cap on cells read in one call, whatever the rows ask for.

Returns:
    The table's row and column counts, then one line per row, then what was left out.

Errors:
    ``is_error`` for an unknown handle, an element that changed since the snapshot, or
    an element that is not a table -- which is reported with what it is instead, so a
    wrong handle and a stale handle do not look the same.

Input parameters:

- `handle` (string, required)
- `max_cells` (integer)
- `row_count` (integer)
- `row_start` (integer)
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `java_click` (~334 tokens)

Click an element: a button, a menu item, a check box, a link, a tree node.

By default this uses the element's **accessibility action**, which is what the
application itself exposes: no mouse moves, no window comes to the front, and it works
where there is no interactive desktop. Use `simulate=True` when the accessibility
action is ignored — it drives real mouse input, which requires the window to come to
the front and cannot work from a service session, and it needs real geometry (cells in
some tables report `bounds = -1`, and there is no point to click).

The element is checked before it is clicked: role, name, position among siblings and
bounds are compared against what the snapshot recorded, and if anything differs
\**nothing is clicked** — the difference is reported instead. That is deliberate: a
click that lands on whatever moved into the position is worse than a message.

Args:
    session_id: From java_attach_window.
    handle: The element to click.
    click: "left", "double" or "right".
    simulate: Drive real mouse input instead of the accessibility action.

Returns:
    What was clicked and how, so the next call knows what state the window is in.

Errors:
    ``is_error`` for an unknown handle, for an element that changed since the
    snapshot, and for a ``click`` value that is not one of the three.

Input parameters:

- `click` (string)
- `handle` (string, required)
- `session_id` (string, required)
- `simulate` (boolean)

Output parameters:

- `result` (string)

### `java_set_text` (~263 tokens)

Type into a text field, text area or editable combo box.

This sets the text rather than appending to it, and it waits for the field to report
the new value before returning, so a following read sees what was typed. By default it
goes through the accessibility action; `simulate=True` types real keystrokes instead,
which is the fallback when the action is refused.

Use java_fill_form instead when several fields need values: it is one round trip and
it reports each field separately.

The element is verified before anything is typed, exactly as java_click describes.

Args:
    session_id: From java_attach_window.
    handle: The field to type into.
    value: The text to set.
    simulate: Type real keystrokes instead of using the accessibility action.

Returns:
    What was set, and a reminder to re-read the field to confirm.

Errors:
    ``is_error`` for an unknown handle, an element that changed, or a field that
    refused the text — the message says to try ``simulate=True`` when that is the
    likely answer.

Input parameters:

- `handle` (string, required)
- `session_id` (string, required)
- `simulate` (boolean)
- `value` (string, required)

Output parameters:

- `result` (string)

### `java_select` (~330 tokens)

Select an option in a combo box, list or tab set — or a row or cell in a table.

Two shapes, and which one you want is told by the arguments:

\* `option="Tennis"` selects by the text the application shows, for a combo box, a list
  or a tab.
\* `row=2` selects a whole table row, and `row=2, column=1` selects one cell. Table
  selection goes through the table's own accessible selection, which is the path the
  application responds to — pyjab's docs are explicit that the cells `get_cell()`
  returns often report `bounds = -1` and cannot be clicked.

The element is verified before anything is selected, exactly as java_click describes.

Args:
    session_id: From java_attach_window.
    handle: The combo box, list, tab set or table.
    option: Text to select. Ignored when a row is given.
    row: Row index to select, 0-based.
    column: Column index, for one cell rather than the whole row.
    simulate: Use simulated input where the action supports it.

Returns:
    What was selected, and how.

Errors:
    ``is_error`` for an unknown handle, an element that changed, an option the control
    does not have, or neither an option nor a row given.

Input parameters:

- `column`
- `handle` (string, required)
- `option` (string)
- `row`
- `session_id` (string, required)
- `simulate` (boolean)

Output parameters:

- `result` (string)

### `java_fill_form` (~273 tokens)

Fill several fields in one call: a list of ``{"handle": ..., "value": ...}``.

A form is usually several fields, and setting them one at a time costs a round trip
each. This is the same verification and the same typing, applied in order, with one
reply that says what happened to every field.

It **does not stop at the first failure**. A form that is half filled with no report is
worse than one that is half filled with one: every field is attempted, and each line
says whether it worked and why not.

Args:
    session_id: From java_attach_window.
    fields: ``[{"handle": "e:1a2b", "value": "admin"}, ...]``, applied in order.
    simulate: Type real keystrokes instead of using the accessibility action.

Returns:
    One line per field — set, or the reason it was not — and a count.

Errors:
    ``is_error`` only for an unknown session, an empty list, or a malformed entry.
    A field that fails is reported in the reply, because that is a result rather than
    a failed call.

Input parameters:

- `fields` (array, required)
- `session_id` (string, required)
- `simulate` (boolean)

Output parameters:

- `result` (string)

### `java_send_keys` (~535 tokens)

Send a keyboard shortcut — ``alt+y``, ``ctrl+shift+s`` — to the window.

Use it for an action the application only exposes as a chord: a menu accelerator, a
toolbar shortcut, a keystroke a canvas-drawn control listens for.

\**Nothing is focused and nothing is clicked.** The chord goes to whichever window
currently has the keyboard focus. That is deliberate on pyjab's side: clicking a
control to focus its window would activate that control, and a shortcut usually exists
to avoid exactly that.

So if a call seems to do nothing, the window probably does not have focus — and
\**attaching does not focus it** (measured in pyjab#180). Call
\`java_focus_window(session_id=...)` first and read what it reports: it says whether the
window *came forward*, which is the fact this tool needs. (Before that tool existed
this reply said to click something harmless first; that workaround is gone from the
shipped text, because it activated whatever it landed on and went through a locator
that could have gone stale — which is the thing a shortcut exists to avoid.)

Chords are written with `+` between key names and are case-insensitive:
\`ctrl`, `alt`, `shift`, `tab`, `enter`, `escape`, `spacebar`, the arrow keys, and
single letters or digits. A name that is not a key is an error rather than a silent
no-op — a wrong chord acts on somebody else's application and cannot be taken back.

This needs pyjab 1.11.0, which released `JABDriver.send_keys`
(gaozhao1989/pyjab#168 went in after 1.10.0). The call is probed rather than assumed,
like every pyjab capability here: an install below the pin is told which release has
the method instead of raising an `AttributeError`.

Args:
    session_id: From java_attach_window.
    keys: The chord, such as "alt+y".

Returns:
    What was sent, and the reminder that a shortcut's effect has to be looked for —
    this tool cannot see what the application did with it.

Errors:
    ``is_error`` for an unknown session, a chord whose key names pyjab does not
    re…

Input parameters:

- `keys` (string, required)
- `session_id` (string, required)

Output parameters:

- `result` (string)

### `java_focus_window` (~508 tokens)

Bring an attached window to the front, and report whether it actually came forward.

Use it before `java_send_keys`, whose chord goes to whichever window has the keyboard
focus: **attaching a window does not give it focus** (measured in pyjab#180), so a
shortcut can otherwise land somewhere else entirely.

\**The reply is the point, not the action.** "I asked for focus" and "the window is now
in front" are two different facts, and this tool answers with the second one. It does so
by reading back the state pyjab left behind, because Windows' `SetForegroundWindow` has
no useful return of its own. So there are three answers, not two:

\* the window **is** the foreground window now;
\* it **is not** — a normal answer rather than a failure, because Windows refuses
  foreground activation from a non-interactive session (a service, a scheduled task) and
  no amount of retrying changes that. An error would be wrong here: nothing failed, and
  the caller has a fact it can act on;
\* pyjab answered something that is neither `True` nor `False`, so this server **cannot
  say** — reported as such rather than filed under "did not come forward", because "I
  could not tell" is not a measurement.

It is not free: **when the window is not already in front**, pyjab sends a space
keystroke first, which is what Windows' foreground-activation rules require of a process
that wants the foreground — and that keystroke goes to whatever currently has the focus.
A caller that cannot tolerate a stray keystroke elsewhere should not call this.

This needs pyjab 1.11.0, which released `JABDriver.focus()`. The call is probed rather
than assumed, like every pyjab capability here: an install below the pin is told which
release has it instead of raising an `AttributeError`.

Args:
    session_id: From java_attach_window.

Returns:
    Whether the window is now the foreground window, said plainly, with the next step.

Errors:
    ``is_error`` only for an unknown session, or a pyjab below the pin — which…

Input parameters:

- `session_id` (string, required)

Output parameters:

- `result` (string)

### `java_wait_for` (~496 tokens)

Wait for something to happen, instead of asking too early and being told no.

Three conditions, chosen by what you pass:

\* `window_title="A Dialog"` — wait until a Java window with that title (fnmatch, so
  \`"*Progress*"` works) exists. Use it after an action that opens one.
\* `handle=..., text_equals="Saved"` — wait until that element's text *is* this.
\* `handle=...` alone — wait until its text *changes* from what it reads when the wait
  starts, and report both values.

\**A timeout is a result, not an error.** The reply says the condition was not met and
what was seen instead, because "not yet" is information an agent can act on — it can
wait longer, look somewhere else, or take a snapshot — while an error reads as a
failed call. pyjab's own waits raise; this one reports, and the difference is
deliberate for a tool a model is driving.

The waiting happens here rather than inside pyjab, and that is also deliberate: the
bridge has exactly one owning thread, so a wait that ran *on* it would block every
other call for its whole duration. This polls between calls instead, which also means
a client can cancel it.

Args:
    session_id: From java_attach_window.
    window_title: Title (fnmatch) of a window to wait for.
    handle: An element whose text to watch.
    text_equals: The text to wait for, with a handle.
    timeout: Give up after this many seconds.
    poll_interval: Seconds between checks. A window wait re-lists the Java windows
        on every poll; on an install below the pin that listing is the
        ``pyjab-inspect`` subprocess rather than a bridge call, so raise this.

Returns:
    Whether the condition was met, how long it took, and what was seen.

Errors:
    ``is_error`` only for an unknown session or handle, or for no condition at all.
    A timeout is reported in the reply, not as an error.

Input parameters:

- `handle` (string)
- `poll_interval` (number)
- `session_id` (string, required)
- `text_equals` (string)
- `timeout` (number)
- `window_title` (string)

Output parameters:

- `result` (string)

### `java_screenshot` (~280 tokens)

A PNG of the window, or of one element inside it.

Use it when the accessibility tree is not enough — a chart, a canvas, a layout problem
— and remember what it cannot do: **a picture is not readable structure.** Anything
you want to act on should be found in the tree with java_snapshot or java_find, because
that is where the handles and the names are. Canvas-drawn interfaces are the case where
this is the only thing that works, and the only thing that is not there.

With a handle, the element is verified first, for the same reason an action is: a
picture of whatever moved into the position would mislead exactly as a click on it
would.

Args:
    session_id: From java_attach_window.
    handle: Optional element to photograph instead of the whole window.

Returns:
    The image itself, as an image content block. Nothing else: a caption would be
    text, and everything a caller needs to know is in the arguments it passed.

Errors:
    ``is_error`` for an unknown session or handle, an element that changed, or a
    window whose geometry is not reported — there is nothing to grab then, and saying
    so beats an empty image.

Input parameters:

- `handle` (string)
- `session_id` (string, required)

## Diagnostics

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

## Score history

- 2026-11-05: 86
- 2026-11-04: 71
- 2026-11-03: 70
- 2026-11-02: 70
- 2026-11-01: 69
- 2026-10-31: 69
- 2026-10-30: 83
- 2026-10-29: 83
- 2026-10-28: 67
- 2026-10-27: 67
- 2026-10-26: 66
- 2026-10-25: 81
- 2026-10-24: 80
- 2026-10-23: 80
- 2026-10-22: 79
- 2026-10-21: 76
- 2026-10-20: 76
- 2026-10-19: 76
- 2026-10-18: 76
- 2026-10-17: 61
- 2026-10-16: 76
- 2026-10-15: 76
- 2026-10-14: 49
- 2026-10-13: 34
- 2026-10-12: 49

## Common questions

### What is the io.github.gaozhao1989/pyjab-mcp server?

io.github.gaozhao1989/pyjab-mcp is listed in the public MCP registry as io.github.gaozhao1989/pyjab-mcp. Drive Java desktop apps from an AI agent, through the JVM's own accessibility tree. This page covers its PyPI package (pyjab-mcp).

### Is the io.github.gaozhao1989/pyjab-mcp server safe to use?

io.github.gaozhao1989/pyjab-mcp scores 86 out of 100 on VerifyMCP. We found no known CVEs affecting it as of 5 November 2026. Its build provenance is signed and verified. That is a record of what we were able to check automatically, not an endorsement. The category breakdown on this page shows every signal behind the number, including the ones we could not confirm.

### What tools does the io.github.gaozhao1989/pyjab-mcp server expose?

io.github.gaozhao1989/pyjab-mcp exposes 16 tools: java_diagnostics, java_list_windows, java_attach_window, java_snapshot, java_find, and 11 more. Their descriptions and schemas cost roughly 5,665 tokens of context every time the server is loaded.

### Is the io.github.gaozhao1989/pyjab-mcp server still maintained?

io.github.gaozhao1989/pyjab-mcp is still listed as active in the MCP registry. We last reached this channel on 5 November 2026. Those dates come from our own scans of the registry and the channel itself, not from anything the publisher announced.

### What licence is the io.github.gaozhao1989/pyjab-mcp server under?

io.github.gaozhao1989/pyjab-mcp declares the MIT licence, which is OSI-approved. That covers the source only, and says nothing about the cost of any service it calls.

### What did VerifyMCP test on the io.github.gaozhao1989/pyjab-mcp server?

VerifyMCP ran 16 checks on io.github.gaozhao1989/pyjab-mcp, covering Malware scan, Known CVEs, Install scripts, Dependency health and 12 more. Endpoint Security and Transport & Reachability do not apply to a PyPI package, so they were not tested. The score does not measure everything, and the scoring documentation lists what it leaves out.

## Links

- PyPI project: https://pypi.org/project/pyjab-mcp/
- Socket report: https://socket.dev/pypi/package/pyjab-mcp
- Repository: https://github.com/gaozhao1989/pyjab-mcp
- Changelog RSS feed: https://verifymcp.io/servers/gaozhao1989-pyjab-mcp/pyjab-mcp.xml
- Changelog JSON feed: https://verifymcp.io/servers/gaozhao1989-pyjab-mcp/pyjab-mcp/changelog.json
- Trust report (JSON): https://verifymcp.io/servers/gaozhao1989-pyjab-mcp/pyjab-mcp.json
- HTML version of this page: https://verifymcp.io/servers/gaozhao1989-pyjab-mcp/pyjab-mcp
