Repository navigation
Releases: gaozhao1989/pyjab-mcp
Release list
v0.3.1
A release about this repository's description of itself, and about what its published artifact
actually contains. Nine changes, and not one of them alters what the server does when it drives
a window. Two are worth naming. A documentation audit by a reader who had not written any of it found
that the table of guards in docs/ARCHITECTURE.md was two guards behind -- a table headed "the rules
that are enforced rather than remembered", which a reader takes as the list of them -- and that the
shipped sdist could not run two of its own checks, because the record one of them reads and the Java
fixture another compiles were not in the manifest at all. Both are fixed, and the first is fixed by
making the guard check its own table, so the next guard cannot fall behind it in silence. The rest is a
corrective pass over claims a reader could check and find false, plus the privacy work of 2026-10-12:
the scan template no longer asks for a competitive analysis on a public tracker, and the harness's own
state and security material are excluded from git and from the sdist by both mechanisms.
One subject: what this project says it is waiting for, and where its own documents claim things
live. Every entry below was found by exporting this repository's shipped documentation to a reader
who had not written it, and each is a sentence a reader could have checked and found false. Nothing
here changes what the server does; one changes a message a user reads.
Fixed
-
The message for an open requirement no longer points at a tracker item that does not exist.
Requirement.message()ended "See the issue above for its state", and the class has five fields --
key,blocks,wanted,why,expected_in-- none of which can name an issue, so the sentence
promised a document nobody could open. It now names the requirement and says plainly that no
requirement is open. Two things worth saying exactly: this is not the below-pin message, which
predates_the_pin()builds and which was not touched (git log -S "predates it. Upgrade it"
over the release is empty); and withREQUIREMENTSempty the branch is unreachable today, so this
is a latent ambiguity corrected rather than a live wrong answer. (issue #93) -
The guards table in
docs/ARCHITECTURE.mdwas two guards behind --check_release_l3.pyand
check_release_unpublished.pyhad no row in a table headed "the rules that are enforced rather than
remembered", which a reader takes as the list of them. The rows are added, and
tools/check_documented_surface.pynow checks the table in both directions, so the next guard cannot
be added without one: a guard with no row fails, and a row whose script was deleted fails too. Two
cases intests/test_guards.pyprove the check can fail, and removing a row from the real page was
shown to turn it red before this was committed. -
docs/ARCHITECTURE.mdsaidjava_send_keys"answers with requirement R4 where it does not".
R4 was retired by pyjab 1.11.0, and the below-pin reply names a release, never a requirement id --
so a reader looking for R4 in the message would not find it. It now says the reply names the release
that has the API. -
Three places still described "what pyjab still owes this project". Nothing is owed: all five
entries in the readiness check report RESOLVED andREQUIREMENTSis empty. The README comment, the
docs/ARCHITECTURE.mdrow and the CI step name now say what the script actually reports -- which
pyjab APIs the pin expects, and whether it resolves them. -
docs/RELEASING.mdsaid both install channels are "inREADME.md". Only the PyPI one is; the
GitHub channel (pip install "git+https://...") is documented indocs/RELEASING.mditself. -
The market-scan template claimed it "travels in the published sdist". It does not: the sdist
contains no.github/, andcheck_dist_contents.pynever required it. The self-containment rule
stays and its reason is now the true one -- the file is published in the repository and is read by
whoever runs the scan, and that reader cannot open a maintainer note either.
Changed
-
pyjab 1.12.0's marker for this project is answered, which it had to be: the guard that reads those
markers refuses to pass until somebody writes down what was done about it, somainwas red and every
open pull request with it. Nothing needed changing -- nothing here ever imported the removed module,
andpyjab>=1.11.0already admits 1.12.0 -- and the two prose notes that mentioned it are corrected
in the shape each file deserves, with the answer recorded where the guard can read it. (pyjab#244) -
The market-scan template keeps the report local. It used to say "this issue is the record of that
scan's inputs", and the first scan followed it: a competitive analysis -- other projects' download
figures, repository measurements, quotations from their documentation -- went into a comment on a
public issue. The report now stays in the maintainer's notes; the issue carries the reminder, the
accepted candidates and any decision with its reasoning; and the template says outright that
competitive names and figures are not to be written on a tracker. -
The harness's own state directory and security material are excluded from git and from the sdist.
The state directory joins the local-only list intools/check_local_only_files.py, which fails unless a
path is excluded by both.gitignoreandMANIFEST.in; environment files, private keys and
credential files are excluded by both as well, with a note in the guard recording that the pattern list
is not covered by its own comparison and why. The files themselves name the patterns -- this entry
does not, because a reader of the sdist has never seen that directory and a guard refuses a shipped file
that cites it.
v0.3.0
A release about the gap between what this project says and what it can show. Eight changes, of
which two change what happens when the server runs: attaching to the same window twice no longer
retires a session that named it, and a new tool answers a question this server used to work around.
The other six are corrections to what the repository says about itself -- a capability claim a
measurement had already refuted, four guards that answered a different question than the one they were
asked, a question about pyjab's data that had been pre-registered for the release that would answer it,
an artifact named after a file it does not hold, a constraint stated without its reason, and a guard
that reported PASSED for the half of itself it had never run.
A batch about the gap between what this project says and what it can show. Seven changes. Two
of them change what happens when the server runs: attaching to the same window twice no longer
retires a session that named it, and a new tool answers a question this server used to work
around. The other five are corrections to what the repository says about itself — a capability
claim a measurement had already refuted, four guards that answered a different question than the
one they were asked, a question about pyjab's data that had been pre-registered for the release
that would answer it, an artifact named after a file it does not hold, and a constraint stated
without its reason. Those five have something in common: each was a sentence a reader could check,
and would have found false.
Added
java_focus_windowbrings a window forward and reports which fact actually happened. "I
asked for focus" and "the window is in front" are two different facts, and the tool answers with
the second: pyjab 1.11.0'sJABDriver.focus()reports whether the window is the foreground
window afterwards, because Windows' own call returns nothing useful. So there are three answers
rather than two. The window is now in front, and a chord should land in it. The window did
not come forward, which is reported as a normal result and not a failed call — Windows
refuses foreground activation from a non-interactive session, a service or a scheduled task, and
no amount of retrying changes that — and the reply points at the tools that do not need the
window in front,java_send_keys' chord excepted. And if pyjab's answer is neitherTruenor
False, the server says it cannot tell, rather than filing that under "it did not come
forward". One caveat belongs with it: when the window is not already in front, the activation
sends a keystroke first, which goes to whatever currently has focus.java_send_keys' reply no
longer advises clicking something harmless to get focus, because there is now a real answer to
"the chord did not arrive", anddocs/ARCHITECTURE.md's tool table carries the new row. The
call is probed rather than assumed, so an install below the pin is told which release has it.
(issue #57)
Fixed
-
check_local_only_filesrefuses a tree where git cannot be asked, instead of printing PASSED.
The guard reported "every one is ignored by git and excluded from the sdist" in a tree where
git check-ignorehad never run:_git_ignores()returnsNonewhen git is unavailable, and its
caller only complained onFalse. Worse, it also asked git only when the path existed on disk, and
git answers fine for a path that is not there -- so in every fresh worktree, and in every CI
checkout, where none of the five local-only paths exist, the call was skipped and the pass was
vacuous. Both are now refusals that name the half that did not run. (issue #84) -
The README and the server's instructions no longer claim JavaFX. Three shipped places said
this server reaches JavaFX — the README's summary line, its "It works on" paragraph, and the
INSTRUCTIONSstring an MCP client shows a model before any tool call, which is the one that
matters most, because a model told JavaFX is reachable will try it. It is not reachable: JavaFX
on Windows publishes its accessibility through UI Automation, not Java Access Bridge, so a
JavaFX window is not a Java window as far as JAB is concerned. The measurement is pyjab's rather
than this project's — a JavaFX window whose contents were known, against an equivalent Swing
application as the control in the same run
(pyjab#227) — and pyjab's own 1.11.1 section
records the same correction to its own README. Reaching JavaFX would need a UI Automation client
inside pyjab, which is a different mechanism rather than a version to wait for, so the wording
now states the mechanism and what is supported instead of predicting what happens when this
server is pointed at a JavaFX target. No code and no pin change: 1.11.1 changes nothing about
pyjab's library, only what it says about itself. (issues #74, #79) -
Attaching to the same window twice no longer invalidates a session that named it. One JAB
context per window is deliberate, so a second attach to a window already bound released the
earlier binding — correctly — and then issued a new window id. A session still named the old
one, and every later call on it answeredwindow 'w1' is not attached, though the caller had
done nothing wrong and could only find out by calling it. The id is reused now when the handle
is already bound, so attaching twice is idempotent for the id a caller holds; the earlier
binding is still released, which is the part that must not change. A different window still
gets a fresh id. (issue #71) -
Four guards refuse an argument or an input they cannot honour instead of answering a
different question. Each of these exited 0 while checking something else: an unknown argument
totools/check_pyjab_api_surface.py— a typo of its one real flag — ran the whole repository
check, because the argument was never read;--record ""to
tools/check_changelog_immutable.pyread as falsy and ran the plain check, which is the
opposite operation; a pyjab API surface check ran without the installed version, so the one
verdict that needs it was skipped while the report still saidPASSED— "I could not read it"
was reported as "nothing is wrong"; and a missing
tools/released_changelog_sections.jsonprintedreleased sections recorded: 0and passed,
which turns the released-section freeze off with a green exit. All four now fail, naming the
input they could not honour. (issue #65) -
The
children_countquestion is answered, and a case now catches a re-collapse. The record
reader writesint(raw.get("children_count") or 0), so a missing orNonevalue would read as
0and a refused property read would be indistinguishable from having no children. The trigger
this project pre-registered for pyjab 1.11.0 has now fired and the answer is that the two cannot
collapse:as_record()returns an unconditionalchildren_countkey, its value is anintfrom
the accessibility structure, and a refused read raises before that dictionary is built — so a
refusal aborts the walk rather than becoming a record that reads as empty. A contract case now
reads the value expression rather than the key name and refuses anything but a bare attribute
read, which is the shape a re-collapse would take; it parses source text, so it needs no bridge
and runs in every leg of the default suite as well as the weekly drift job. (issue #58) -
The release workflow's artifact says what it holds. The L3 job's last step was called
"Upload the log" and produced an artifact callede2e-log, whose path is the fixture's Java
source; downloading it yieldsPyjabTestApp.javaand no log. The log is the job's console
output, whichpytest -v -sputs there and GitHub already keeps. The step and the artifact are
now named for the application a failing run was driving —e2e-app-src— so someone debugging a
live-window failure no longer goes looking for a log and finds a.javafile. Nothing a user
runs changes. (issue #69)
Changed
- The README says what the no-injection constraint is for. It stated the property — this
server never modifies, restarts or injects into the target application — without saying what
that buys. One comparison paragraph now names the alternative mechanism, an agent attached to an
already-running JVM or named with-javaagentat startup, which puts a tool's code inside a
process it did not launch and does not control; this server loads nothing into the application
and reads the accessibility tree through Java Access Bridge from outside it. It also says where
the JDK has a direction on that mechanism: JEP 451 shipped in
JDK 21 as the preparation stage, an already-running JVM now warns when an agent is loaded, and
the release that would disallow it by default has not been named. No competitor is named and no
claim is made about what users want. (issue #76)
v0.2.1
A patch release about telling the truth. Every fix in it is one of this project's own claims
turning out to be wrong: a diagnostics message that reported two pyjab capabilities as missing when
1.11.0 had already released them, a bundle smoke test that never asked the bundle to do anything, a
citation rule blind to Windows paths, and a release gate that could not fail the release it was
guarding. None of it changes what the server does on a working Windows desktop; all of it changes
what this repository says about itself, which is the part a user has to be able to trust.
Fixed
java_diagnosticsno longer reports capabilities the installed pyjab has. pyjab 1.11.0
released the last two APIs this project was waiting on —JABDriver.send_keys()(R4) and
Win32Utils.set_window_foreground()withJABDriver.focus()/JABElement.focus()(R5) — so the
pin ispyjab>=1.11.0and both moved intoRETIRED_REQUIREMENTS.java_send_keysneeded no code
change at all: it probes for the method rather than importing it, which is why the contract tests
pinned the shape before the release and 1.11.0 surprised nothing here. An install below the pin is
still told which release has it. This was verified by parsing the released wheel, because
check_pyjab_readiness.pycannot measure either API off Windows — it reports
cannot be imported here: No module named 'pythoncom', which is a blind spot and not an absence.
java_focus_window, which R5 unblocks, is deliberately not built yet (issue #57). (issue #56)- The bundle smoke test asks the bundle to do something. It drove
initialize()and
tools/listand asserted the tool list, so a bundle that started, listed 15 tools and then died
on its first request still passed. It now callsjava_diagnosticsover the wire and asserts the
report came back, not merely that no error did. The failure path was shown rather than asserted:
with a built bundle's entry script mutated so the request raised, the old assertion passed and
the new one failed; so did answering with an error instead of raising. (issue #47) - A citation written with Windows separators is caught. The published-references rule matched
forward slashes only, so a backslash-separated citation of a maintainer-only file — for example
themanifest-schema.mdthat lives only under the notes, which a reader on Windows could follow
— was invisible to it. It normalises separators now, and a bare drive spelling that carries such
a file's tail fires for the same reason the forward-slash absolute form already did. One limit is recorded
rather than solved: the resolving half of that rule needs the local-only files on disk, so CI —
which checks out none of them — exercises the name half only, and the guard's own "0 on this
repository" test passes vacuously there. (issues #48, #49) - The changelog guard's sources are authoritative, and the release path is strict. An explicit
--changelog PATHthat did not exist was silently skipped in favour of the fallback chain, so a
caller asking about one specific file was told it was fine; andrelease.ymlran the guard
without--strictwhileci.ymlran it with it, which made the tag — where it matters most —
the one path where the check could not fail the release. (issue #64)
Changed
- The suite says which guards it invokes, and invokes the ones it did not. The command-line
coverage test covered four of the eleven guards; three of the rest were already invoked through
their own argument forms, and four were genuinely missing. They now run, the argument-taking ones
as refusal cases, and the docstring names which guards are left to CI and why. (issue #40) packagingis declared, not inherited.tools/build_mcpb.pyevaluates PEP 508 markers with
it, and it used to arrive only throughbuildandtwine: the day either stopped carrying it,
the packer would fail naming a missing module rather than the missing declaration. It is now
named in the dev extra for its own sake. This makes the dependency explicit rather than newly
present — it was already installed by those two lines. (issue #41)- The release documentation says what the publish gate actually guarantees. It claimed no agent
could satisfy thepypienvironment's review, including one holding the maintainer's credentials.
0.2.0 showed that is false as written: the maintainer approved in the session and the decision was
carried out through the API. What the gate guarantees is that a human is asked and the answer is
kept — the decision is recorded on the deployment — and the failure it still prevents is an
agent publishing on its own initiative. (issue #52)
v0.2.0
A minor release with a new way to install the server, and two release gates that now refuse
instead of relying on someone remembering. The bundle is the headline: pyjab-mcp installs as a
single .mcpb that carries its dependency closure per supported interpreter, and CI now executes
it on Windows for every one of them — the check that found a cp310 tree missing exceptiongroup,
which would have killed a 3.10 user's server at import while every static check passed. The release
pipeline gained two refusals (a version already on PyPI, and a tag whose commit has no green L3 run),
the guards gained one more (a sentence about pyjab's marker convention that the changelog check
used to read as a marker), and the bundle's own pages stopped claiming a release set that does not
have it.
Changed
-
The release pipeline refuses a tag for a version that is already on PyPI. The pre-flight
that nothing is already published was a line the maintainer's agent remembered, and the
pipeline never did it — whilegithub-releaseneeds onlyverify, so a duplicate version
reached that job beforepypirefused it, producing a GitHub release with no matching
package: exactly the half-published state the pre-flight exists to prevent. A newverifystep
asks PyPI's JSON API for the version and hands the answer to
tools/check_release_unpublished.py, which proceeds on 404, refuses on 200 (naming the
version and the project URL, and the version the index reports when it disagrees), and
refuses as unread on anything else — a network failure during a release is not permission
to continue — and an unparsable 200 is still a refusal, but its line leads with what was seen
(an HTTP 200 whose body is not PyPI's JSON) rather than claiming the index named the version.
The GitHub-release answer is printed but never changes the verdict, because a re-run of a tag
whose release already exists is legitimate. A dry run warns instead of failing, as the version
guard and the L3 gate already do. The wiring is pinned bytests/test_guards.pyin both
directions: the step's call has to be the last command it runs, and the job graph that makes
github-releaserun beforepypiis asserted, so the hazard cannot return by accident.
docs/RELEASING.mdalso records the repair for a legitimate refusal — a version already on
PyPI because a job afterpypifailed — which is
gh workflow run publish-registry.yml -f version=X.Y.Z, not a new tag. -
The release pipeline refuses a tag whose commit has no successful
windows-gui.ymlrun.
"L3 before the tag" was a step a person remembered, and the GUI job is not a gate on a pull
request, so nothing noticed when it was skipped — and PyPI cannot take a release back. The
verifyjob now asks the Actions API and fails when no run at the tagged commit succeeded,
printing the run it accepted or every candidate it rejected. "No green run" and "could not read
the answer" are different verdicts and both refuse, because a check that cannot tell them
apart passes exactly when it is blind. A dry run warns instead of failing — including on an
answer the gate could not understand: a body that was not the documented shape used to raise an
uncaught traceback and fail the job, and it now refuses as unread and warns on a dry run
like every other refusal. -
docs/RELEASING.mdsays who confirms what. The pipeline owns everything checkable — the
four places the version lives, the six refusing guards, the artifact, the changelog freeze, and
a green L3 run on the tagged commit — and the human decides once whether this version ships; the
pypienvironment's required review enforces that decision rather than re-checking it. One step
is deliberately not in the pipeline and is now written as such: the pre-flight that nothing
is already published, because the tag is what starts the run.
Added
-
The foreground gap is tracked as data rather than as a note (R5).
java_send_keyssends a
chord to whatever has the keyboard focus, and the only supported way to give a window focus
today is an interactive call — a click, which activates whatever it lands on. That is the side
effect a shortcut exists to avoid, so "click something harmless first" is a workaround, and
java_diagnosticsnow reports the gap itself: a probe for a public foreground API, the
requirement behind it, and the upstream issue
(pyjab#180) it is waiting on. The day pyjab
ships one,tools/check_pyjab_readiness.pysays READY and ajava_focus_windowtool is an
hour of work. -
pyjab-mcpships as an MCPB bundle. v0.1.0–v0.1.2 predate the bundle and carry no assets;
every release cut since carries one. The remaining
distribution item was a file a user can install without a build step, and the packer is
tools/build_mcpb.pyrather than arun:block so that what it produces is pinned offline by
tests/test_packaging.py— the version it declares is read frompyjab_mcp.__version__and
re-read from the file it wrote, the platform iswin32, the entry point the manifest names and
the vendored directory the shippedmcpb/entry.pyprepends are both asserted inside the zip,
and an emptydist/is refused with the command that fixes it instead of becoming an empty
bundle. A newmcpbjob builds it from the release's own wheel and attaches it with
gh release upload; onworkflow_dispatchthe same job builds and validates the bundle and
keeps it as a workflow artifact, because a job that can only run on a tag is a job nobody can
verify. It does not bundle a Python interpreter: Windows, a 64-bit CPython 3.10-3.13 and
a JDK with Java Access Bridge enabled are still required — the bundle vendors the dependency
tree for every supported CPython minor, becausepydantic-core,cffi,rpds-pyand
pywin32ship per-interpreter wheels, and it says so in its own manifest rather than claiming
"3.10 or newer" — and MCPB enforces no permissions, so the server runs with the full privileges
of the user who installed it.README.md,docs/CLIENTS.mdanddocs/RELEASING.mdsay so
where a user reads it, and on an interpreter the bundle carries nothing for,mcpb/entry.py
stops with a sentence naming what it found and what the file holds instead of letting an
ImportErrorout of a native module name a DLL. Each vendored tree is also checked against the
target interpreter's PEP 508 markers before the bundle is written —pip --python-version
picks eligible wheels and leaves environment markers alone, soanyio's
exceptiongroup; python_version < "3.11"was missing from the 3.10 tree entirely and the server
would have died at startup on the one interpreter that tree exists for. The check names the
tree, the distribution that asked and the requirement as written; the packer installs what it
reports by name until every tree is satisfied, and a tree it cannot satisfy fails the build
rather than shipping. It is a satisfaction check and not a presence one — a tree carrying a
version its own metadata excludes is reported and repaired — it follows extras to a fixed
point, it answers for every patch level of the target minor because the patch level is the
user's, and a marker naming something a bundle cannot know (the OS build the user will run) is
carried and said so rather than filled in from the build machine, which is the wrong-machine
category the check exists for. -
The MCPB bundle is now executed in default CI, on every supported interpreter. Every
other statement about the bundle was a statement about its shape — the manifest validates,
the members are right, the version is injected — and that is exactly where the last blocking
defect lived: thecp310tree was missingexceptiongroup, so a 3.10 user's server died at
startup while every static check passed, including one that compared the declared range with the
vendored directory names (issue #37). Two new jobs close it.mcpb-buildbuilds the wheel
and the bundle once and uploads it as an artifact;mcpb-runs— a matrix over 3.10, 3.11,
3.12 and 3.13 onwindows-latest— downloads that one artifact, unpacks it and drives a real
MCP handshake (initialize()andtools/list) through the bundle's ownserver/entry.pywith
the interpreter of the leg, over the samestdio_client/ClientSessionmechanism
tests/test_stdio_server.pyuses for the console script. The subprocess is started with
-I -S, so it sees the standard library and the bundle and no site-packages at all: without
that, the 3.10 leg's own install supplies theexceptiongroupthe bundle is missing and the
broken tree passes.tests/test_mcpb_bundle.pyskips only whenPYJAB_MCP_BUNDLEis unset, so
the default suite on a machine that has not built a bundle is unaffected, and it fails —
never skips — when the variable is set and the bundle is missing or not runnable, because a
skipped leg reports green while nothing ran. The wiring is pinned bytests/test_guards.pyby
effect rather than by presence: the step's command, the variable it sets and the value, the
artifact it downloads, all four matrix legs, the absence of anif:on the jobs or the step and
of apaths:filter on the workflow, and the skip/fail boundary itself — proved by running the
test file with the variable unset, then with it pointing at a file that is not there, and then
at a real minimal bundle so the test's body runs in the pin rather than only on a runner. What
this proves is that the bundle starts and answerstools/liston Windows with a plain
interpreter; it is not L3, it uses no JDK and no Java Access Bridge, and it exercises no tool
call against a real window —docs/RELEASING.mdsays so where the release proce...
v0.1.2
A patch release with three things a user can feel: java_send_keys now sends chords where the installed pyjab provides the API rather than refusing unconditionally (so the release that answers R4 makes it work with no change here), java_snapshot says why it can show fewer children than an element reports instead of printing an unexplained children=3, and the console script gained --help and --version. The sections below are the detail.
Added
-
--helpsays where to get help, and what the free version includes. The plan's F1 asked
for a commercial-support line insidejava_diagnostics's output; that output is structured
findings an agent reads to work out what is wrong with an environment, and a support address
in it is prose addressed to somebody who is not there. It goes where a person is looking
instead —--helpand the README — with the sentence a user actually wonders about: no
licence key, no quota, nothing held back. Two tests pin that decision in both directions. -
The enterprise gaps are documented rather than implied. The plan's F4 reserved five
capabilities architecturally and never wrote them down: remote transport, multi-tenancy, an
audit log, general batch execution, and CI integration. The README now lists each with what
adding it would take — a policy, an authentication story, a decision about user data — because
"we did not think about it" reads worse than "this is deliberately stdio-only, one session at
a time, and the audit trail is your client's transcript". The capability-boundaries section
above it is about what the substrate cannot reach; this one is about choices.
Changed
-
Aggregator directories are no longer a release step. They were a criterion in the plan; the
plan now records why they were dropped, because the reasons were measured rather than assumed.
Installation never depended on them —pip install pyjab-mcp(verified in a clean venv) and
pip install "git+https://github.com/gaozhao1989/pyjab-mcp"(verified, 15 tools) are the two
channels, and both are in the README. The directories are downstream consumers that scrape
the official Registry's API, and that entry is live — so the one channel that costs nothing is
already open. Of the four the plan named, one had paused submissions, one is not a form
at all (a Streamable HTTP endpoint this project deliberately does not have, or a local MCPB
bundle), and two ask for an account or a third-party issue.docs/RELEASING.mdkeeps the
measured state of each in a new Discovery section, so revisiting is a decision rather than a
rediscovery. -
The aggregator guidance was wrong in two places, measured rather than remembered. The
Registry's own documentation says aggregators scrape its read-only API on an infrequent
schedule — so the listing follows from the registry entry rather than from a form. Checked
each of the four on 2026-10-10: PulseMCP's submissions are paused ("we will reopen as soon
as it is ready"), Smithery is not a form at all (a public Streamable HTTP URL, or a local
MCPB bundle — the first is blocked by this project's deliberate stdio-only decision), LobeHub
takes a GitHub issue whose template asks for the official Registry name, and mcp.so has a
/submitform.docs/RELEASING.mdnow says that instead of what it assumed. -
java_send_keyssends chords where pyjab provides the API. pyjab closed
pyjab#168 as completed andmasterhas
JABDriver.send_keys,JABElement.send_keysandWin32Utils.send_keys, while the newest
release on PyPI was still 1.10.0. This tool would not have noticed: it raised the
requirement unconditionally, the one capability here that never probed, so the release that
answers R4 would have changed nothing. It now probes withgetattrand calls what it finds —
the mechanism that made the window listing anddetach()start working the day 1.10.0 landed
(#15).The reply states the two things a caller gets wrong, both from pyjab's documentation: nothing
is focused and nothing is clicked (the chord goes to whatever has the keyboard focus, because
clicking to focus would activate the control), and an unrecognised key name is an error —
reported as an argument problem, naming the key, rather than with the environment hint.(This entry belongs to #15, whose pull request shipped without it: the changelog edit's anchor
did not match and nothing asserted that it had. It is recorded here rather than in the released
0.1.1 section, which is frozen and published.)
Fixed
-
A snapshot could print
children=3with nothing under it and give no reason. pyjab reports
an element's total child count while a walk yields the children the tree exposes, so a
collapsed node or an unselected tab legitimately shows fewer children than it reports — pyjab's
own finding, pyjab#173. Four situations then
looked identical: children that are not visible, a walk stopped by its limit, a bug in this
renderer, or a read that failed.java_snapshotnow marks the difference where it exists —children=3 (+2), on the node and on
the window line — with one legend explaining that the first number is the element's own total.
When the walk was also cut short the legend says so, because the gap then has two possible
causes and the reply must not pick one for the reader. Both appear only when thechildren
field is printed, so a default snapshot does not grow a paragraph it cannot use.This project's own
only_interactivefilter is deliberately outside the comparison: a container
dropped from the rendering was still walked, and counting it as a gap would blame the
application for our choice. Six tests cover the marker, the legend, the truncated variant, the
filter case, and the ordinary case where nothing is added.
v0.1.1
A patch release, because the MCP Registry registers a PyPI package by reading a line out of
its README — and 0.1.0's README did not have it. PyPI had already published and a published
version's metadata cannot be edited, so registering 0.1.0 was not possible: the registry
answered PyPI package 'pyjab-mcp' ownership validation failed. The server name 'io.github.gaozhao1989/pyjab-mcp' must appear as 'mcp-name: io.github.gaozhao1989/pyjab-mcp' in the package README. This is pyjab's rule for it: if anything reached PyPI, cut a patch release
rather than move a tag.
Fixed
-
The registry publish failed at 0.1.0, after PyPI had already published. The
registry
job ranpython -m pytestin a job that never installed Python —No module named pytest,
exit 1, and the registry validator never even ran. The package went live and its metadata did
not, and there was no way to publish the metadata alone: re-running the tag's workflow replays
the tag's workflow file, and a new tag would mean a new PyPI release.The metadata publish now lives in its own workflow, callable from
release.ymland
dispatchable by hand (gh workflow run publish-registry.yml -f version=0.1.0), which is the
repair path — and the likely one, given the registry's own warning that its data may be reset.
The Python check moved to where Python is installed; the registry's validator stays where the
registry is.Two things this says about the release path, and both are now written down rather than
remembered: a job that publishes should run only the checks it can actually run, and the
registry runs after PyPI because it points at a package that has to exist — so its
failures arrive when they can no longer be fixed by re-releasing. -
pyjab-mcp --helpprints nothing and exits 0. The console script takes no arguments, so
--helpstarts the stdio server, reads EOF and stops. It had been written into the after-tag
confirmation step indocs/RELEASING.md, where it looked like a check and was not one; the
step now installs the published wheel and lists its tool surface, which is a check. The
missing--help/--versionis worth adding and is not a release rule.
v0.1.0
The first release. 15 MCP tools that let an agent read and drive a Java desktop
application through the JVM's own accessibility tree: windows, a bounded snapshot with
handles, element search and inspection, text, tables, clicking, typing, selection, waiting
and screenshots — with every action verified against the element the snapshot recorded before
anything is clicked or typed into.
Requires Windows, a JDK with Java Access Bridge enabled, and pyjab>=1.10.0. The one tool
that cannot work is java_send_keys: pyjab has no public API for a key sequence, so it
reports that requirement and the way round it rather than pretending send_text can express
alt+y (tracked as gaozhao1989/pyjab#168).
Added
-
M9: the release pipeline, and the registry validator that caught a real mistake.
.github/workflows/release.ymlis tag-driven:verify(the version guard, the suite, every
guard, a real build andtwine check) runs before anything is uploaded; then PyPI via OIDC
trusted publishing with no token in the repository, then the MCP Registry, then the GitHub
release with the changelog section as its notes.workflow_dispatchrehearses the whole
verify job without publishing.tools/check_release_version.pyrefuses a release whose tag, package version,server.json
and changelog do not agree — the four places that name a release, checked before the one
action that cannot be undone. It also writes the release notes, so the notes and the check
cannot disagree about what a release contains.The registry's validator rejected the first
server.json, and it was right. The published
JSON schema at2025-07-09spells the package fieldsregistry_type/runtime_hintand
mentions no other version; the registry API requiredregistryType, and treated the
deprecated$schemaas a failure to publish. Had this surfaced during a real release it would
have been discovered after PyPI had the package, which cannot be undone. The file now follows
the current schema (2025-12-11), theregistryjob runsmcp-publisher validatebefore
publish, andPYJAB_MCP_REGISTRY=1runs the same validator from the test suite. The static
schema, a published changelog and memory all disagreed with the authority; only the authority
was right.docs/RELEASING.mdhas the procedure and the two things that must exist before the first tag:
a PyPI pending publisher forpyjab-mcp(workflowrelease.yml, environmentpypi) and the
registry's GitHub namespace verification. -
pyjab 1.9.0 is the floor, and M2 is built on it. The release shipped the tree walk
(JABElement.walk→JABTreewith a truncation flag) andJABElement.as_record(), which
is what the precheck called R3 and whatjava_snapshotneeded. The pin moved to
pyjab>=1.9.0in the same change that started using them, and each API'sreleased_inin
tools/check_pyjab_api_surface.pywas verified against the 1.9.0 wheel and the 1.8.0 sdist
rather than remembered. -
java_snapshot: a bounded, handle-carrying view of the tree.depth,max_nodesand
only_interactivebound both the walk and the view; when either is cut short the reply
says so and names the way to narrow it, because a truncated view that does not admit it is
indistinguishable from a window with less in it. The default budget is asserted under
4,000 estimated tokens in the tests — a release criterion, not a hope. -
Element handles that hold no JAB references (D2). A handle is an opaque id mapped to
{session, locator, expected identity}; nothing is held between calls, the element is
re-found withfind_element_by_xpath, checked against what the snapshot recorded (role,
name, position among siblings, bounds), and released immediately — in afinally, because
the check runs between taking the reference and giving it back. The reference-counting fake
intests/_fakedriver.pyfails the tests if either half of that is wrong.Two consequences, both deliberate: eviction releases nothing (there is nothing to release,
which is the whole argument for this design over a table of live references), and an element
whose position the application does not report gets no handle rather than one that might
resolve to something else. -
java_list_windowsworks, through the documentedpyjab-inspect windows --json. pyjab
1.9.0 has the function this needs (inspector.bridge_without_window) and does not publish
it — it is not in that module's__all__. The CLI built on it is public, so the listing
runs through that, in a subprocess, and R1 stays recorded. A cost is stated rather than
hidden: a listing costs a process, not a bridge call. -
The pyjab drift mechanism (AGENTS.md §1a).
tools/check_pyjab_changelog.pyfails while
a pyjab changelog line markedAffects: pyjab-mcphas no entry inacknowledged.yaml, and
fails for an entry whose marker has gone — so the answer cannot outlive the question.
tests/contract/test_pyjab_contract.pypins the three shapes this project actually reads
(the walk,as_record()'s fields, the pump), and.github/workflows/pyjab-drift.ymlruns
the whole suite againstpyjab@masterweekly onwindows-latest, which catches drift
before a release rather than after.acknowledged.yamlrecords the four pre-convention
commits as unenforced context, because calling them acknowledgements would be a claim the
check cannot verify. -
Guards — five checks that make the project's rules mechanical instead of remembered.
Modelled on pyjab'stools/check_*.py, and on its reasoning: a rule kept by hand is kept
until somebody is in a hurry. Each prints a report, exits non-zero on a violation, and is
pinned bytests/test_guards.py, which proves it can fail — a checker whose failure
path has never run will not run the day it matters.tools/check_pyjab_api_surface.py— every pyjab name this package touches must be
recorded inSURFACEwith its status and the release that has it. An unrecorded
touchpoint, a private name, a stale entry, a public entry with no version, and above all
an import of an unreleased API each fail the build. This is the M1 precheck's lesson
(the checkout is not the wheel) made a build failure. It found three things on its first
run, one of which was this project readingpyjab.__version__: the version now comes
from distribution metadata, because the dunder was a touchpoint no rule allowed.tools/check_documented_surface.py— the tool table and the module map in
docs/ARCHITECTURE.mdmust match whatcreate_server()registers and what is on disk.
The page gained a module map and ashipscolumn so that both directions are checkable:
a tool documented as available but not registered, one registered but documented as a
later milestone, or a module missing from the map all fail.tools/check_local_only_files.py— one list of local-only paths checked against both
mechanisms that keep them out, withgit check-ignoreas the behaviour rather than
.gitignoreas the claim.tools/check_dist_contents.py— the artifact rather than the rules: a built sdist must
contain no local-only file and must contain the package, the tests and the guards; the
wheel must contain only the package, no stalebuild/copies, and anentry_points.txt
naming the console script.tools/check_pyjab_readiness.py— the wait for a pyjab release, made re-checkable. It
asks the installed pyjab the same questions the code asks, reportsREADY/
NOT READYper requirement, and exits non-zero under--requirewhile something is
missing.UNKNOWNcounts as not ready: "I could not measure it" is not "it is there".
-
A
guardsjob and apackagejob in CI. The guards run as separate steps so a
failure names the rule it broke; the package job builds the sdist and wheel, runs
twine check, checks the artifact withcheck_dist_contents.py, and installs the wheel
into a clean environment to prove the console script reaches real code.buildand
twineare declared in[project.optional-dependencies].devrather than installed in a
workflow step, which is how pyjab's own workflows drifted apart once. -
java_diagnosticsnow reports a fourth pyjab API fact,non-destructive detach. The
R2 gap was recorded in prose and then inREQUIREMENTS; the report now also measures it,
so "what is pyjab still missing" has one answer in one place. -
The M1 precheck, run against the installed wheel rather than the sibling checkout.
Plan §11 gated M1 on answering "is pyjab's public API enough?", and the answer needed
care: the sibling checkout is 62 commits past thev1.8.0tag, soJABElement.walk,
JABTree,pyjab.inspectorandWin32Utils.java_process_image_pathsall appear to
exist while none of them is in any released version. The audit, the two gaps it
found, and what each blocks are recorded inAGENTS.md§6 and encoded as
REQUIREMENTSinsrc/pyjab_mcp/backend.pyso the documentation and the message a user
sees cannot drift apart. -
BridgeThread(src/pyjab_mcp/bridge.py) — one thread owns the bridge. Java Access
Bridge delivers accessibility events to the thread that calledWindows_run(), and that
thread must service its own message queue; a pump on another thread fails by never seeing
a window that opens later, with no exception. The class constructs the backend on its own
thread, pumps while idle, drains a work queue, and hands callers aFuture(or an
awaitable, so the event loop is never blocked by a cross-process call). A backend that
cannot be opened is reported as a state: queued work is released with the reason and
later calls fail fast instead of hanging.close()reports whether the thread actually
joined, because a thread blocked inside a JAB cal...