pyjab-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyjab_mcp-0.1.0/CHANGELOG.md +405 -0
- pyjab_mcp-0.1.0/LICENSE +21 -0
- pyjab_mcp-0.1.0/MANIFEST.in +32 -0
- pyjab_mcp-0.1.0/PKG-INFO +237 -0
- pyjab_mcp-0.1.0/README.md +201 -0
- pyjab_mcp-0.1.0/acknowledged.yaml +76 -0
- pyjab_mcp-0.1.0/docs/ARCHITECTURE.md +196 -0
- pyjab_mcp-0.1.0/pyproject.toml +92 -0
- pyjab_mcp-0.1.0/setup.cfg +4 -0
- pyjab_mcp-0.1.0/src/pyjab_mcp/__init__.py +18 -0
- pyjab_mcp-0.1.0/src/pyjab_mcp/__main__.py +42 -0
- pyjab_mcp-0.1.0/src/pyjab_mcp/backend.py +1094 -0
- pyjab_mcp-0.1.0/src/pyjab_mcp/bridge.py +377 -0
- pyjab_mcp-0.1.0/src/pyjab_mcp/diagnostics.py +306 -0
- pyjab_mcp-0.1.0/src/pyjab_mcp/handles.py +324 -0
- pyjab_mcp-0.1.0/src/pyjab_mcp/server.py +1254 -0
- pyjab_mcp-0.1.0/src/pyjab_mcp/tree.py +360 -0
- pyjab_mcp-0.1.0/src/pyjab_mcp/windows.py +127 -0
- pyjab_mcp-0.1.0/src/pyjab_mcp.egg-info/PKG-INFO +237 -0
- pyjab_mcp-0.1.0/src/pyjab_mcp.egg-info/SOURCES.txt +50 -0
- pyjab_mcp-0.1.0/src/pyjab_mcp.egg-info/dependency_links.txt +1 -0
- pyjab_mcp-0.1.0/src/pyjab_mcp.egg-info/entry_points.txt +2 -0
- pyjab_mcp-0.1.0/src/pyjab_mcp.egg-info/requires.txt +10 -0
- pyjab_mcp-0.1.0/src/pyjab_mcp.egg-info/top_level.txt +1 -0
- pyjab_mcp-0.1.0/tests/_fakebackend.py +231 -0
- pyjab_mcp-0.1.0/tests/_fakedriver.py +349 -0
- pyjab_mcp-0.1.0/tests/conftest.py +17 -0
- pyjab_mcp-0.1.0/tests/contract/test_pyjab_contract.py +270 -0
- pyjab_mcp-0.1.0/tests/e2e/test_live_window.py +534 -0
- pyjab_mcp-0.1.0/tests/test_backend_act.py +113 -0
- pyjab_mcp-0.1.0/tests/test_backend_api.py +317 -0
- pyjab_mcp-0.1.0/tests/test_backend_read.py +233 -0
- pyjab_mcp-0.1.0/tests/test_backend_table.py +147 -0
- pyjab_mcp-0.1.0/tests/test_backend_tree.py +143 -0
- pyjab_mcp-0.1.0/tests/test_bridge_thread.py +193 -0
- pyjab_mcp-0.1.0/tests/test_diagnostics.py +221 -0
- pyjab_mcp-0.1.0/tests/test_guards.py +655 -0
- pyjab_mcp-0.1.0/tests/test_handles.py +110 -0
- pyjab_mcp-0.1.0/tests/test_packaging.py +191 -0
- pyjab_mcp-0.1.0/tests/test_server_tools.py +705 -0
- pyjab_mcp-0.1.0/tests/test_stdio_server.py +80 -0
- pyjab_mcp-0.1.0/tests/test_tree.py +317 -0
- pyjab_mcp-0.1.0/tools/check_changelog_immutable.py +177 -0
- pyjab_mcp-0.1.0/tools/check_dist_contents.py +199 -0
- pyjab_mcp-0.1.0/tools/check_documented_surface.py +224 -0
- pyjab_mcp-0.1.0/tools/check_local_only_files.py +154 -0
- pyjab_mcp-0.1.0/tools/check_pyjab_api_surface.py +697 -0
- pyjab_mcp-0.1.0/tools/check_pyjab_changelog.py +283 -0
- pyjab_mcp-0.1.0/tools/check_pyjab_readiness.py +172 -0
- pyjab_mcp-0.1.0/tools/check_release_version.py +134 -0
- pyjab_mcp-0.1.0/tools/fetch_test_app.py +115 -0
- pyjab_mcp-0.1.0/tools/verify_diagnostics_jdk.py +111 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to pyjab-mcp are documented here, following
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/) and [Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
Releases are immutable: a published version is never edited. Anything still in flight is
|
|
7
|
+
under **Unreleased** until it is tagged.
|
|
8
|
+
|
|
9
|
+
## Unreleased
|
|
10
|
+
|
|
11
|
+
Nothing yet. This repository has not shipped anything since 0.1.0.
|
|
12
|
+
|
|
13
|
+
## [0.1.0] - 2026-10-10
|
|
14
|
+
|
|
15
|
+
**The first release.** 15 MCP tools that let an agent read and drive a Java desktop
|
|
16
|
+
application through the JVM's own accessibility tree: windows, a bounded snapshot with
|
|
17
|
+
handles, element search and inspection, text, tables, clicking, typing, selection, waiting
|
|
18
|
+
and screenshots — with every action verified against the element the snapshot recorded before
|
|
19
|
+
anything is clicked or typed into.
|
|
20
|
+
|
|
21
|
+
Requires Windows, a JDK with Java Access Bridge enabled, and `pyjab>=1.10.0`. The one tool
|
|
22
|
+
that cannot work is `java_send_keys`: pyjab has no public API for a key sequence, so it
|
|
23
|
+
reports that requirement and the way round it rather than pretending `send_text` can express
|
|
24
|
+
`alt+y` (tracked as gaozhao1989/pyjab#168).
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
* **M9: the release pipeline, and the registry validator that caught a real mistake.**
|
|
29
|
+
`.github/workflows/release.yml` is tag-driven: `verify` (the version guard, the suite, every
|
|
30
|
+
guard, a real build and `twine check`) runs before anything is uploaded; then PyPI via OIDC
|
|
31
|
+
trusted publishing with no token in the repository, then the MCP Registry, then the GitHub
|
|
32
|
+
release with the changelog section as its notes. `workflow_dispatch` rehearses the whole
|
|
33
|
+
verify job without publishing.
|
|
34
|
+
|
|
35
|
+
`tools/check_release_version.py` refuses a release whose tag, package version, `server.json`
|
|
36
|
+
and changelog do not agree — the four places that name a release, checked before the one
|
|
37
|
+
action that cannot be undone. It also writes the release notes, so the notes and the check
|
|
38
|
+
cannot disagree about what a release contains.
|
|
39
|
+
|
|
40
|
+
**The registry's validator rejected the first `server.json`, and it was right.** The published
|
|
41
|
+
JSON schema at `2025-07-09` spells the package fields `registry_type`/`runtime_hint` and
|
|
42
|
+
mentions no other version; the registry API required `registryType`, and treated the
|
|
43
|
+
deprecated `$schema` as a failure to publish. Had this surfaced during a real release it would
|
|
44
|
+
have been discovered *after* PyPI had the package, which cannot be undone. The file now follows
|
|
45
|
+
the current schema (`2025-12-11`), the `registry` job runs `mcp-publisher validate` before
|
|
46
|
+
`publish`, and `PYJAB_MCP_REGISTRY=1` runs the same validator from the test suite. The static
|
|
47
|
+
schema, a published changelog and memory all disagreed with the authority; only the authority
|
|
48
|
+
was right.
|
|
49
|
+
|
|
50
|
+
`docs/RELEASING.md` has the procedure and the two things that must exist before the first tag:
|
|
51
|
+
a PyPI pending publisher for `pyjab-mcp` (workflow `release.yml`, environment `pypi`) and the
|
|
52
|
+
registry's GitHub namespace verification.
|
|
53
|
+
|
|
54
|
+
* **pyjab 1.9.0 is the floor, and M2 is built on it.** The release shipped the tree walk
|
|
55
|
+
(`JABElement.walk` → `JABTree` with a truncation flag) and `JABElement.as_record()`, which
|
|
56
|
+
is what the precheck called R3 and what `java_snapshot` needed. The pin moved to
|
|
57
|
+
`pyjab>=1.9.0` in the same change that started using them, and each API's `released_in` in
|
|
58
|
+
`tools/check_pyjab_api_surface.py` was verified against the 1.9.0 wheel and the 1.8.0 sdist
|
|
59
|
+
rather than remembered.
|
|
60
|
+
|
|
61
|
+
* **`java_snapshot`: a bounded, handle-carrying view of the tree.** `depth`, `max_nodes` and
|
|
62
|
+
`only_interactive` bound both the walk and the view; when either is cut short the reply
|
|
63
|
+
says so and names the way to narrow it, because a truncated view that does not admit it is
|
|
64
|
+
indistinguishable from a window with less in it. The default budget is asserted under
|
|
65
|
+
4,000 estimated tokens in the tests — a release criterion, not a hope.
|
|
66
|
+
|
|
67
|
+
* **Element handles that hold no JAB references (D2).** A handle is an opaque id mapped to
|
|
68
|
+
`{session, locator, expected identity}`; nothing is held between calls, the element is
|
|
69
|
+
re-found with `find_element_by_xpath`, checked against what the snapshot recorded (role,
|
|
70
|
+
name, position among siblings, bounds), and released immediately — in a `finally`, because
|
|
71
|
+
the check runs between taking the reference and giving it back. The reference-counting fake
|
|
72
|
+
in `tests/_fakedriver.py` fails the tests if either half of that is wrong.
|
|
73
|
+
|
|
74
|
+
Two consequences, both deliberate: eviction releases nothing (there is nothing to release,
|
|
75
|
+
which is the whole argument for this design over a table of live references), and an element
|
|
76
|
+
whose position the application does not report gets **no handle** rather than one that might
|
|
77
|
+
resolve to something else.
|
|
78
|
+
|
|
79
|
+
* **`java_list_windows` works, through the documented `pyjab-inspect windows --json`.** pyjab
|
|
80
|
+
1.9.0 has the function this needs (`inspector.bridge_without_window`) and does not publish
|
|
81
|
+
it — it is not in that module's `__all__`. The CLI built on it *is* public, so the listing
|
|
82
|
+
runs through that, in a subprocess, and R1 stays recorded. A cost is stated rather than
|
|
83
|
+
hidden: a listing costs a process, not a bridge call.
|
|
84
|
+
|
|
85
|
+
* **The pyjab drift mechanism (AGENTS.md §1a).** `tools/check_pyjab_changelog.py` fails while
|
|
86
|
+
a pyjab changelog line marked `Affects: pyjab-mcp` has no entry in `acknowledged.yaml`, and
|
|
87
|
+
fails for an entry whose marker has gone — so the answer cannot outlive the question.
|
|
88
|
+
`tests/contract/test_pyjab_contract.py` pins the three shapes this project actually reads
|
|
89
|
+
(the walk, `as_record()`'s fields, the pump), and `.github/workflows/pyjab-drift.yml` runs
|
|
90
|
+
the whole suite against `pyjab@master` weekly on `windows-latest`, which catches drift
|
|
91
|
+
before a release rather than after. `acknowledged.yaml` records the four pre-convention
|
|
92
|
+
commits as unenforced context, because calling them acknowledgements would be a claim the
|
|
93
|
+
check cannot verify.
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
* **Guards — five checks that make the project's rules mechanical instead of remembered.**
|
|
97
|
+
Modelled on pyjab's `tools/check_*.py`, and on its reasoning: a rule kept by hand is kept
|
|
98
|
+
until somebody is in a hurry. Each prints a report, exits non-zero on a violation, and is
|
|
99
|
+
pinned by `tests/test_guards.py`, which proves it **can** fail — a checker whose failure
|
|
100
|
+
path has never run will not run the day it matters.
|
|
101
|
+
|
|
102
|
+
* `tools/check_pyjab_api_surface.py` — every pyjab name this package touches must be
|
|
103
|
+
recorded in `SURFACE` with its status and the release that has it. An unrecorded
|
|
104
|
+
touchpoint, a private name, a stale entry, a public entry with no version, and above all
|
|
105
|
+
**an import of an unreleased API** each fail the build. This is the M1 precheck's lesson
|
|
106
|
+
(the checkout is not the wheel) made a build failure. It found three things on its first
|
|
107
|
+
run, one of which was this project reading `pyjab.__version__`: the version now comes
|
|
108
|
+
from distribution metadata, because the dunder was a touchpoint no rule allowed.
|
|
109
|
+
* `tools/check_documented_surface.py` — the tool table and the module map in
|
|
110
|
+
`docs/ARCHITECTURE.md` must match what `create_server()` registers and what is on disk.
|
|
111
|
+
The page gained a module map and a `ships` column so that both directions are checkable:
|
|
112
|
+
a tool documented as available but not registered, one registered but documented as a
|
|
113
|
+
later milestone, or a module missing from the map all fail.
|
|
114
|
+
* `tools/check_local_only_files.py` — one list of local-only paths checked against both
|
|
115
|
+
mechanisms that keep them out, with `git check-ignore` as the behaviour rather than
|
|
116
|
+
`.gitignore` as the claim.
|
|
117
|
+
* `tools/check_dist_contents.py` — the artifact rather than the rules: a built sdist must
|
|
118
|
+
contain no local-only file and must contain the package, the tests and the guards; the
|
|
119
|
+
wheel must contain only the package, no stale `build/` copies, and an `entry_points.txt`
|
|
120
|
+
naming the console script.
|
|
121
|
+
* `tools/check_pyjab_readiness.py` — the wait for a pyjab release, made re-checkable. It
|
|
122
|
+
asks the installed pyjab the same questions the code asks, reports `READY` /
|
|
123
|
+
`NOT READY` per requirement, and exits non-zero under `--require` while something is
|
|
124
|
+
missing. `UNKNOWN` counts as not ready: "I could not measure it" is not "it is there".
|
|
125
|
+
|
|
126
|
+
* **A `guards` job and a `package` job in CI.** The guards run as separate steps so a
|
|
127
|
+
failure names the rule it broke; the package job builds the sdist and wheel, runs
|
|
128
|
+
`twine check`, checks the artifact with `check_dist_contents.py`, and installs the wheel
|
|
129
|
+
into a clean environment to prove the console script reaches real code. `build` and
|
|
130
|
+
`twine` are declared in `[project.optional-dependencies].dev` rather than installed in a
|
|
131
|
+
workflow step, which is how pyjab's own workflows drifted apart once.
|
|
132
|
+
|
|
133
|
+
* **`java_diagnostics` now reports a fourth pyjab API fact, `non-destructive detach`.** The
|
|
134
|
+
R2 gap was recorded in prose and then in `REQUIREMENTS`; the report now also measures it,
|
|
135
|
+
so "what is pyjab still missing" has one answer in one place.
|
|
136
|
+
|
|
137
|
+
* **The M1 precheck, run against the installed wheel rather than the sibling checkout.**
|
|
138
|
+
Plan §11 gated M1 on answering "is pyjab's public API enough?", and the answer needed
|
|
139
|
+
care: the sibling checkout is 62 commits past the `v1.8.0` tag, so `JABElement.walk`,
|
|
140
|
+
`JABTree`, `pyjab.inspector` and `Win32Utils.java_process_image_paths` all *appear* to
|
|
141
|
+
exist while **none of them is in any released version**. The audit, the two gaps it
|
|
142
|
+
found, and what each blocks are recorded in `AGENTS.md` §6 and encoded as
|
|
143
|
+
`REQUIREMENTS` in `src/pyjab_mcp/backend.py` so the documentation and the message a user
|
|
144
|
+
sees cannot drift apart.
|
|
145
|
+
|
|
146
|
+
* **`BridgeThread` (`src/pyjab_mcp/bridge.py`) — one thread owns the bridge.** Java Access
|
|
147
|
+
Bridge delivers accessibility events to the thread that called `Windows_run()`, and that
|
|
148
|
+
thread must service its own message queue; a pump on another thread fails by never seeing
|
|
149
|
+
a window that opens later, with no exception. The class constructs the backend on its own
|
|
150
|
+
thread, pumps while idle, drains a work queue, and hands callers a `Future` (or an
|
|
151
|
+
awaitable, so the event loop is never blocked by a cross-process call). A backend that
|
|
152
|
+
cannot be opened is reported as a state: queued work is released with the reason and
|
|
153
|
+
later calls fail fast instead of hanging. `close()` reports whether the thread actually
|
|
154
|
+
joined, because a thread blocked inside a JAB call cannot be cancelled.
|
|
155
|
+
|
|
156
|
+
* **`tests/_fakebackend.py`, and the L1 suite around it.** The fake records the thread ident
|
|
157
|
+
of every call and raises when it is not the owner — the only way to test the
|
|
158
|
+
thread-affinity rule without a Windows desktop — and it also raises on use after close,
|
|
159
|
+
so a test that leaks a call fails here rather than on a user's machine.
|
|
160
|
+
|
|
161
|
+
* **`java_diagnostics`.** Reports the platform, interpreter, installed pyjab, which of its
|
|
162
|
+
public APIs are present, whether Java Access Bridge is enabled, where the bridge DLL is,
|
|
163
|
+
and the state of the bridge thread. Every row carries a status, and a row that could not
|
|
164
|
+
be measured reads `UNAVAILABLE (reason)` — never `0`, never "missing". It never writes to
|
|
165
|
+
the environment: in particular it does not construct pyjab's `Service`, whose constructor
|
|
166
|
+
writes `~/.accessibility.properties`.
|
|
167
|
+
|
|
168
|
+
* **`java_list_windows` and `java_attach_window`.** Wired end to end and exercised by tests
|
|
169
|
+
over a real stdio handshake. On released pyjab they report the missing public API (R1) as
|
|
170
|
+
an actionable error rather than reaching for a private one; `java_attach_window` holds no
|
|
171
|
+
JAB reference, because pyjab has no non-destructive detach (R2) and holding one would
|
|
172
|
+
either leak a Java object per attach or kill the user's application when the session ends.
|
|
173
|
+
|
|
174
|
+
* **Tests for the console script over the real stdio transport.** It catches the seam
|
|
175
|
+
between the package and the client — a module that cannot be executed, a crash before the
|
|
176
|
+
first request, or a stray `print` corrupting the protocol stream on stdout.
|
|
177
|
+
|
|
178
|
+
* **Local agent configuration, deliberately not published.** `.agents/skills/` holds four
|
|
179
|
+
skills that reference pyjab (the dependency rules and the R1/R2/R3 gaps, the
|
|
180
|
+
bridge-threading procedure, the test layers, and this machine's setup). They take effect
|
|
181
|
+
from the working tree without being versioned; `.gitignore` keeps them out of git and
|
|
182
|
+
`MANIFEST.in` prunes them from the sdist.
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
* **L3 in CI: a real window, on a real desktop, on a schedule.** `tests/e2e/` is skipped
|
|
186
|
+
unless `PYJAB_MCP_E2E=1`; `.github/workflows/windows-gui.yml` sets it on a hosted
|
|
187
|
+
`windows-latest` runner, which has an interactive desktop rather than being a service in
|
|
188
|
+
session 0. The application is pyjab's own test app, compiled from the sdist of the
|
|
189
|
+
*installed* pyjab — one version, both sides — and fetched by `tools/fetch_test_app.py`,
|
|
190
|
+
which is a script rather than shell in a workflow for the usual reason.
|
|
191
|
+
|
|
192
|
+
Three things it says, in order of how much it would hurt to get wrong: a snapshot of a real
|
|
193
|
+
accessibility tree contains the controls the application builds; a handle resolves and
|
|
194
|
+
verifies against the real element it was taken from; and **a window that opens after the
|
|
195
|
+
executor has been idle is still seen** — the D3 trap, which no L1 test can catch because
|
|
196
|
+
the fake would have to be wrong in the same way the real thing is.
|
|
197
|
+
|
|
198
|
+
* **`java_diagnostics` checked on JDK 8, 17 and 21.** The same workflow runs a matrix of the
|
|
199
|
+
three, with `tools/verify_diagnostics_jdk.py` asserting not just that a bridge DLL was found
|
|
200
|
+
but that it is the one **paired with that JDK** (JDK 8 keeps it under `jre\bin`, 11+ under
|
|
201
|
+
`bin`). A mismatched pair fails as "pyjab cannot see my window", which is the hardest
|
|
202
|
+
failure in this project to tell apart from a bridge that is simply not enabled.
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
* **M3: `java_find`, `java_get_element` and `java_get_text`.** A snapshot shows the shape of a
|
|
206
|
+
window; these ask questions about it.
|
|
207
|
+
|
|
208
|
+
`java_find` takes exactly one selector — `name`, `role`, `description`, `states` or `xpath`
|
|
209
|
+
(so substrings via pyjab's `contains()`) — and returns a handle per match. A match is only
|
|
210
|
+
given a handle if a locator finds *it* again: the candidates are tried in order, most
|
|
211
|
+
survivable first, and each is resolved and compared before it is kept. A match that no
|
|
212
|
+
locator can pin down is listed **without** a handle, because D2 is explicit that a handle
|
|
213
|
+
which might point at something else is worse than no handle. Every element the search hands
|
|
214
|
+
over is released, including the ones past the result limit — the shape of the leak pyjab's
|
|
215
|
+
own walk had to be fixed for.
|
|
216
|
+
|
|
217
|
+
`java_get_element` reports one element in full and says whether it is still the element it
|
|
218
|
+
was; `java_get_text` reads its text, slices it client-side (pyjab's only public text API
|
|
219
|
+
returns the whole string), and reports how much of the whole it carries. An element with no
|
|
220
|
+
text interface says so, which is a different answer from an empty string and is reported as
|
|
221
|
+
one.
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
* **M4: `java_get_table`.** A Java table read through the JVM's own `AccessibleTable`
|
|
225
|
+
interface — row and column counts, then cell text — which is the capability a platform
|
|
226
|
+
bridge does not carry at all.
|
|
227
|
+
|
|
228
|
+
It never returns the whole table: a page is bounded by `row_start`, `row_count` and a
|
|
229
|
+
`max_cells` budget, and the reply says what it left out and how to ask for it. Three cell
|
|
230
|
+
states are kept apart all the way to the reply, because collapsing them would invent data:
|
|
231
|
+
text, `-` for a cell with no Accessible Text interface, and `?` for a cell whose read
|
|
232
|
+
raised, with a count — "this cell has no text" and "I could not read this cell" are
|
|
233
|
+
different answers (AGENTS.md §2.4). Every cell is released as it is read, one at a time; a
|
|
234
|
+
200-row table read without that is 200 Java objects left behind.
|
|
235
|
+
|
|
236
|
+
Writing it turned up two bugs in itself, both caught by the tests it came with: the page
|
|
237
|
+
size and the table's height shared a name, so asking for one row returned the whole table,
|
|
238
|
+
and the per-cell budget check was dead code because the row count had already been reduced
|
|
239
|
+
to fit it.
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
* **M5: the actions — `java_click`, `java_set_text`, `java_select`, `java_fill_form`.** Each
|
|
243
|
+
one verifies the element and then acts **on the element it verified**, inside one element
|
|
244
|
+
lifetime, so there is no window between the check and the action in which the tree could
|
|
245
|
+
move. When the element has changed, nothing happens and the difference is reported: the
|
|
246
|
+
click that would have landed on whatever moved into the position is a message instead.
|
|
247
|
+
|
|
248
|
+
`java_click` maps left/double/right onto pyjab's accessibility action, mouse
|
|
249
|
+
double-click and right-click, with `simulate=True` for real input where the action is
|
|
250
|
+
ignored — the trade pyjab documents (window comes to the front, needs real geometry, cannot
|
|
251
|
+
work from a service session). `java_set_text` waits for the field to report the value, so a
|
|
252
|
+
following read sees what was typed. `java_select` picks its shape from the arguments: a
|
|
253
|
+
text option for a combo box, list or tab, or a row or cell for a table, which goes through
|
|
254
|
+
the table's accessible selection because that is the path applications respond to.
|
|
255
|
+
`java_fill_form` is one call for several fields, and it does **not** stop at the first
|
|
256
|
+
failure: a half-filled form with a report is worth more than one without.
|
|
257
|
+
|
|
258
|
+
* **`java_send_keys` is registered and answers with the requirement it is waiting on (R4).**
|
|
259
|
+
Every key helper pyjab has is private (`Win32Utils._press_key`, `_send_keys`, ...) and
|
|
260
|
+
`send_text` types text rather than a modifier chord, so `alt+y` has no public
|
|
261
|
+
implementation. Driving the private helpers would mean depending on names nobody promised,
|
|
262
|
+
in the one area where a wrong key does something irreversible. The reply names the
|
|
263
|
+
requirement **and** the way round it: `java_set_text` for text, or the menu item that
|
|
264
|
+
exposes the same action, found with `java_find(role="menu item")` and clicked.
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
* **M8: client setup and the boundary of what this can do.** `docs/CLIENTS.md` has the
|
|
268
|
+
configuration for Claude Desktop, Claude Code, Cursor and VS Code — the file each one reads,
|
|
269
|
+
the JSON shape, and the two calls to make first. It leads with the failure that accounts for
|
|
270
|
+
most setups: a client starts the server without your `PATH`, so a virtual environment's
|
|
271
|
+
`pyjab-mcp` is "not found" until the absolute path is used.
|
|
272
|
+
|
|
273
|
+
A README section states what this **cannot** do, because that is the half people discover
|
|
274
|
+
the hard way: canvas-drawn interfaces (no accessible children at all), Java inside a browser,
|
|
275
|
+
applets in a plugin host, non-Java applications, and anything pixel-based. `java_screenshot`
|
|
276
|
+
will show a picture of a canvas; a picture is not something you can act on, and the section
|
|
277
|
+
says so.
|
|
278
|
+
|
|
279
|
+
**`server.json`** for the MCP Registry, with a test that checks it against the fields the
|
|
280
|
+
registry's schema actually requires — it uses snake_case (`registry_type`, `runtime_hint`),
|
|
281
|
+
which is not what an earlier draft of that schema said, so it was read rather than
|
|
282
|
+
remembered — and that the version it names is the release the package builds. A registry
|
|
283
|
+
entry pointing at a version that does not exist is worse than no entry.
|
|
284
|
+
|
|
285
|
+
A **Support** section, as `docs/PLAN.md` F2/F5 planned: free support through GitHub issues,
|
|
286
|
+
commercial and enterprise support through the address this package already publishes in its
|
|
287
|
+
metadata. The open-source package stays complete — no licence key, no quota, nothing held
|
|
288
|
+
back — which the section says outright.
|
|
289
|
+
|
|
290
|
+
### Changed
|
|
291
|
+
|
|
292
|
+
* **The pin is `pyjab>=1.10.0`, and R1 and R2 are retired.** 1.10.0 added
|
|
293
|
+
`pyjab.list_java_windows()` — dicts with `hwnd`, `title` and `pid` — and
|
|
294
|
+
`JABDriver.detach()`, which releases the root element's JAB reference and forgets the window
|
|
295
|
+
**without terminating its process**. Those were the two gaps this project waited through M1
|
|
296
|
+
and M2 for: the listing no longer needs pyjab's CLI in a subprocess, and a session can be
|
|
297
|
+
released without killing the application it was observing.
|
|
298
|
+
|
|
299
|
+
`PyjabBackend.close()` now detaches every window instead of dropping the dict, and attaching
|
|
300
|
+
a window that is already bound releases the earlier binding rather than holding a second
|
|
301
|
+
context for it. The CLI listing stays as the fallback for an install below the pin — a floor
|
|
302
|
+
can be worked around — and nothing supported reaches it.
|
|
303
|
+
|
|
304
|
+
**1.10.0 was published while this project's CI was running.** The readiness check reported it
|
|
305
|
+
`READY` and a test that asserted the previous world failed in the same run; both were right.
|
|
306
|
+
It is the drift machinery doing what it was built for, and the reason the pin, the registry
|
|
307
|
+
and the readiness check are all verified rather than remembered: each `released_in` here was
|
|
308
|
+
checked against the installed wheel, and the 1.8.0 and 1.9.0 sdists for what came earlier.
|
|
309
|
+
|
|
310
|
+
* **`java_diagnostics` reports a Windows-only API call off Windows as unavailable**, rather
|
|
311
|
+
than letting pyjab's descriptive `ImportError` cross the tool boundary: the library is
|
|
312
|
+
importable everywhere and only *runs* on Windows, and that is an environment answer.
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
* **D2's verification is split into what blocks and what is reported, because a real run showed
|
|
316
|
+
why it has to be.** The first end-to-end run through the tools set a text field to `"hello"`,
|
|
317
|
+
then could not read it back: the edit had *renamed* the element. `JTextField("1122233455")`
|
|
318
|
+
with no `setName` reports the text as its accessible name — Swing's
|
|
319
|
+
`AccessibleJTextComponent.getName()` falls back to the content — and D2 as written compared
|
|
320
|
+
the name, so an edit invalidated the handle that made it.
|
|
321
|
+
|
|
322
|
+
Blocking now: role, position among siblings, and bounds when both sides report them.
|
|
323
|
+
Reported: the name, **for the four roles whose name is their content** (`text`,
|
|
324
|
+
`password text`, `label`, `table cell`). For every other role a name change still blocks,
|
|
325
|
+
because a renamed button at the same position is a different button — which is the case the
|
|
326
|
+
check exists for. The reply says which of the two happened.
|
|
327
|
+
|
|
328
|
+
* **A handle from `java_find` is now built from a walk path rather than from a name.**
|
|
329
|
+
Same evidence, other half: a locator containing `[@name='1122233455']` stops resolving the
|
|
330
|
+
moment the field's content changes, so the handle could not read back what it had written.
|
|
331
|
+
Search results are placed against one bounded walk of the window and given the same
|
|
332
|
+
position-based locators a snapshot produces; only when the walk cannot place an element does
|
|
333
|
+
it fall back to a name locator, which resolves now but may not survive the next edit. One
|
|
334
|
+
walk serves every result in the call.
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
* **R3 is retired, R1 and R2 are restated against 1.9.0** (`AGENTS.md` §6, and the
|
|
338
|
+
`REQUIREMENTS` tuple in `backend.py` that the tools' error messages come from). A
|
|
339
|
+
requirement that outlives its release is a lie the code tells in its own messages.
|
|
340
|
+
|
|
341
|
+
* **`tools/check_pyjab_readiness.py` distinguishes a release question from a pin question.**
|
|
342
|
+
The tree walk is answered by the pin (`pyjab>=1.9.0` requires the release that has it), not
|
|
343
|
+
by a probe that cannot run off Windows; R1 and R2 are still release questions and still
|
|
344
|
+
report `NOT READY`.
|
|
345
|
+
|
|
346
|
+
* **The API-surface guard follows a driver passed as an argument.** Extracting the walk and
|
|
347
|
+
the re-resolution into functions that take the driver — which is what makes the ownership
|
|
348
|
+
rule testable off Windows — moved ten touchpoints out of the analyzer's reach until they
|
|
349
|
+
were declared in `DRIVER_PARAMETERS`. The guard noticed the moment they changed, which is
|
|
350
|
+
the property that matters: it fails loudly rather than silently checking less.
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
* **The MCP SDK floor is `mcp>=2.0`.** The 2.x line renamed `FastMCP` to
|
|
354
|
+
`mcp.server.mcpserver.MCPServer` and `mcp.server.fastmcp` now raises an `ImportError`
|
|
355
|
+
explaining the move, so the decorator shape and the import path are both 2.x. `mcp>=1.0.0`
|
|
356
|
+
was a claim the code did not honour.
|
|
357
|
+
|
|
358
|
+
* **`pyjab` stays at `>=1.8.0` for M1, and that is deliberate.** M1's tools do not call the
|
|
359
|
+
tree-walk API; M2 raises the floor to `>=1.9.0` in the same change that first calls it,
|
|
360
|
+
because a floor that permits a version without the API is a lie.
|
|
361
|
+
|
|
362
|
+
* **`README.md` state is now "M1 shipped, blocked on a pyjab release"** rather than
|
|
363
|
+
"pre-alpha skeleton", and the honest boundary is stated: no tool here has yet driven a
|
|
364
|
+
live Java application.
|
|
365
|
+
|
|
366
|
+
* **`backend.probe_public_api()` reads pyjab's version from distribution metadata.** It used
|
|
367
|
+
`pyjab.__version__`, a dunder in somebody else's package — which the API-surface guard
|
|
368
|
+
refused to record, which is the guard doing its job on its first run. The version it
|
|
369
|
+
reports is therefore the *installed* one, which is the question diagnostics is asking.
|
|
370
|
+
|
|
371
|
+
* **`docs/ARCHITECTURE.md` gained a module map and a `ships` column** in the tool table, so
|
|
372
|
+
that both directions of "the page and the code agree" are checkable, and `README.md`
|
|
373
|
+
gained a "Working on it" section listing the guards.
|
|
374
|
+
|
|
375
|
+
### Fixed
|
|
376
|
+
|
|
377
|
+
* **A real table's cells report no text, and M4 was reading `None` for all of them.** L3
|
|
378
|
+
found it: the table's shape came through (4 columns, ≥5 rows) and every cell read as
|
|
379
|
+
`None`. Swing's `AccessibleJTableCell` puts the cell's *value* in its accessible **name**,
|
|
380
|
+
so `java_get_table` now falls back to the name when there is no AccessibleText — and says
|
|
381
|
+
which cells came from where, because a value should never be presented as something it is
|
|
382
|
+
not. Without this, a whole table of real values would have been reported as unknown.
|
|
383
|
+
|
|
384
|
+
* **The handle path called a method ``JABDriver`` does not have.** `resolve_driver` released
|
|
385
|
+
the re-resolved element with `driver.release_jabelement(element)`; the method belongs to
|
|
386
|
+
**`JABElement`**, and `JABDriver` has never had one. It would have raised `AttributeError`
|
|
387
|
+
inside the `finally` of every resolution on Windows — the first real desktop run was the
|
|
388
|
+
first time that line executed.
|
|
389
|
+
|
|
390
|
+
Two things had to be true for this to survive the L1 suite, and both were: the adapter
|
|
391
|
+
guessed, and `tests/_fakedriver.py` was shaped like the guess, so the fake agreed with the
|
|
392
|
+
mistake instead of with pyjab. The fake now puts the release on the element, where pyjab
|
|
393
|
+
puts it, and `tests/contract/` asserts the API the adapter actually calls — including the
|
|
394
|
+
signature that lets an element release itself.
|
|
395
|
+
|
|
396
|
+
* **The diagnostics job treated "no Java windows" as a failure.** `pyjab-inspect windows`
|
|
397
|
+
exits 1 when JAB sees nothing, and on a bare runner that is the expected answer rather than
|
|
398
|
+
an error — `UNAVAILABLE` is not `0`, and "nothing is there" is a measurement (AGENTS.md
|
|
399
|
+
§2.4). The step reported it and failed; it now reports it and carries on, so the assertion
|
|
400
|
+
behind it actually runs.
|
|
401
|
+
|
|
402
|
+
* **The L3 snapshot walked three levels and the controls are seven deep.** The first real run
|
|
403
|
+
found the frame, the layered panes, the menu bar and "Main panel" — and nothing inside it,
|
|
404
|
+
so the two assertions about a button and a table failed. The test walks 12 now, and says why:
|
|
405
|
+
a snapshot's default depth is a budget for an agent, not a fact about the tree.
|
pyjab_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021-2026 Gary Gao and contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
include LICENSE
|
|
2
|
+
include README.md
|
|
3
|
+
include CHANGELOG.md
|
|
4
|
+
include pyproject.toml
|
|
5
|
+
# The record of what was done about each marked pyjab change. It ships so that a source
|
|
6
|
+
# checkout can run its own guards -- tools/check_pyjab_changelog.py reads it, and the guard
|
|
7
|
+
# is not much use if the file it checks is missing from the sdist.
|
|
8
|
+
include acknowledged.yaml
|
|
9
|
+
recursive-include src *.py
|
|
10
|
+
recursive-include tests *.py
|
|
11
|
+
# The guards and verification scripts. sdist only, like pyjab's tools/: they are maintainer
|
|
12
|
+
# tools, and a wheel contains only the package. tools/check_dist_contents.py enforces both
|
|
13
|
+
# halves of that sentence.
|
|
14
|
+
recursive-include tools *.py
|
|
15
|
+
|
|
16
|
+
# ARCHITECTURE.md ships; the two notes below do not. Excluded by name rather than by a
|
|
17
|
+
# pattern over docs/, precisely so that adding a third public page is a one-line change
|
|
18
|
+
# rather than a pattern that has to be reasoned about.
|
|
19
|
+
include docs/ARCHITECTURE.md
|
|
20
|
+
#
|
|
21
|
+
# This file exists for the same reason pyjab has one: an sdist is built from the working
|
|
22
|
+
# tree, so .gitignore does not protect it. A note that is ignored by git and not excluded
|
|
23
|
+
# here is published by the next `python -m build`.
|
|
24
|
+
exclude AGENTS.md
|
|
25
|
+
exclude docs/PLAN.md
|
|
26
|
+
exclude docs/DESIGN.md
|
|
27
|
+
exclude docs/RELEASE_READINESS.md
|
|
28
|
+
|
|
29
|
+
# The local agent configuration -- the skills that describe how this checkout is worked
|
|
30
|
+
# on, and the same reasoning as above. Pruned by directory rather than by file, so a new
|
|
31
|
+
# skill cannot be forgotten here.
|
|
32
|
+
prune .agents
|