Skip to content

Releases: gaozhao1989/pyjab-mcp

v0.3.1

Choose a tag to compare

@github-actions github-actions released this 12 Oct 14:46
1a53d1c

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 with REQUIREMENTS empty 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.md was two guards behind -- check_release_l3.py and
    check_release_unpublished.py had 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.py now 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 in tests/test_guards.py prove the check can fail, and removing a row from the real page was
    shown to turn it red before this was committed.

  • docs/ARCHITECTURE.md said java_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 and REQUIREMENTS is empty. The README comment, the
    docs/ARCHITECTURE.md row 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.md said both install channels are "in README.md". Only the PyPI one is; the
    GitHub channel (pip install "git+https://...") is documented in docs/RELEASING.md itself.

  • The market-scan template claimed it "travels in the published sdist". It does not: the sdist
    contains no .github/, and check_dist_contents.py never 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, so main was red and every
    open pull request with it. Nothing needed changing -- nothing here ever imported the removed module,
    and pyjab>=1.11.0 already 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 in tools/check_local_only_files.py, which fails unless a
    path is excluded by both .gitignore and MANIFEST.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

Choose a tag to compare

@github-actions github-actions released this 11 Oct 16:29
554bafc

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_window brings 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's JABDriver.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 neither True nor
    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", and docs/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_files refuses 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-ignore had never run: _git_ignores() returns None when git is unavailable, and its
    caller only complained on False. 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
    INSTRUCTIONS string 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 answered window '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
    to tools/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.py read 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 said PASSED — "I could not read it"
    was reported as "nothing is wrong"; and a missing
    tools/released_changelog_sections.json printed released sections recorded: 0 and 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_count question is answered, and a case now catches a re-collapse. The record
    reader writes int(raw.get("children_count") or 0), so a missing or None value would read as
    0 and 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 unconditional children_count key, its value is an int from
    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 called e2e-log, whose path is the fixture's Java
    source; downloading it yields PyjabTestApp.java and no log. The log is the job's console
    output, which pytest -v -s puts 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 .java file. 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 -javaagent at 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

Choose a tag to compare

@github-actions github-actions released this 11 Oct 14:33
201e174

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_diagnostics no 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() with JABDriver.focus()/JABElement.focus() (R5) — so the
    pin is pyjab>=1.11.0 and both moved into RETIRED_REQUIREMENTS. java_send_keys needed 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.py cannot 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/list and asserted the tool list, so a bundle that started, listed 15 tools and then died
    on its first request still passed. It now calls java_diagnostics over 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
    the manifest-schema.md that 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 PATH that 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; and release.yml ran the guard
    without --strict while ci.yml ran 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)
  • packaging is declared, not inherited. tools/build_mcpb.py evaluates PEP 508 markers with
    it, and it used to arrive only through build and twine: 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 the pypi environment'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

Choose a tag to compare

@github-actions github-actions released this 11 Oct 01:33
02b392a

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 — while github-release needs only verify, so a duplicate version
    reached that job before pypi refused it, producing a GitHub release with no matching
    package: exactly the half-published state the pre-flight exists to prevent. A new verify step
    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 by tests/test_guards.py in both
    directions: the step's call has to be the last command it runs, and the job graph that makes
    github-release run before pypi is asserted, so the hazard cannot return by accident.
    docs/RELEASING.md also records the repair for a legitimate refusal — a version already on
    PyPI because a job after pypi failed — 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.yml run.
    "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
    verify job 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.md says 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
    pypi environment'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_keys sends 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_diagnostics now 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.py says READY and a java_focus_window tool is an
    hour of work.

  • pyjab-mcp ships 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.py rather than a run: block so that what it produces is pinned offline by
    tests/test_packaging.py — the version it declares is read from pyjab_mcp.__version__ and
    re-read from the file it wrote, the platform is win32, the entry point the manifest names and
    the vendored directory the shipped mcpb/entry.py prepends are both asserted inside the zip,
    and an empty dist/ is refused with the command that fixes it instead of becoming an empty
    bundle. A new mcpb job builds it from the release's own wheel and attaches it with
    gh release upload; on workflow_dispatch the 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, because pydantic-core, cffi, rpds-py and
    pywin32 ship 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.md and docs/RELEASING.md say 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
    ImportError out 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, so anyio'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: the cp310 tree was missing exceptiongroup, 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-build builds 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
    on windows-latest — downloads that one artifact, unpacks it and drives a real
    MCP handshake (initialize() and tools/list) through the bundle's own server/entry.py with
    the interpreter of the leg, over the same stdio_client/ClientSession mechanism
    tests/test_stdio_server.py uses 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 the exceptiongroup the bundle is missing and the
    broken tree passes. tests/test_mcpb_bundle.py skips only when PYJAB_MCP_BUNDLE is 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 by tests/test_guards.py by
    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 an if: on the jobs or the step and
    of a paths: 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 answers tools/list on 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.md says so where the release proce...

Read more

v0.1.2

Choose a tag to compare

@github-actions github-actions released this 10 Oct 12:36
66f8474

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

  • --help says where to get help, and what the free version includes. The plan's F1 asked
    for a commercial-support line inside java_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 — --help and 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.md keeps 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
    /submit form. docs/RELEASING.md now says that instead of what it assumed.

  • java_send_keys sends chords where pyjab provides the API. pyjab closed
    pyjab#168 as completed and master has
    JABDriver.send_keys, JABElement.send_keys and Win32Utils.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 with getattr and calls what it finds —
    the mechanism that made the window listing and detach() 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=3 with 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_snapshot now 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 the children
    field is printed
    , so a default snapshot does not grow a paragraph it cannot use.

    This project's own only_interactive filter 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

Choose a tag to compare

@github-actions github-actions released this 10 Oct 08:51
ead19ca

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 ran python -m pytest in 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.yml and
    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 --help prints nothing and exits 0. The console script takes no arguments, so
    --help starts the stdio server, reads EOF and stops. It had been written into the after-tag
    confirmation step in docs/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/--version is worth adding and is not a release rule.

v0.1.0

Choose a tag to compare

@github-actions github-actions released this 10 Oct 08:40
5a4472e

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.yml is tag-driven: verify (the version guard, the suite, every
    guard, a real build and twine 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_dispatch rehearses the whole
    verify job without publishing.

    tools/check_release_version.py refuses 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 at 2025-07-09 spells the package fields registry_type/runtime_hint and
    mentions no other version; the registry API required registryType, and treated the
    deprecated $schema as 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), the registry job runs mcp-publisher validate before
    publish, and PYJAB_MCP_REGISTRY=1 runs 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.md has the procedure and the two things that must exist before the first tag:
    a PyPI pending publisher for pyjab-mcp (workflow release.yml, environment pypi) 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 → JABTree with a truncation flag) and JABElement.as_record(), which
    is what the precheck called R3 and what java_snapshot needed. The pin moved to
    pyjab>=1.9.0 in the same change that started using them, and each API's released_in in
    tools/check_pyjab_api_surface.py was 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_nodes and
    only_interactive bound 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 with find_element_by_xpath, checked against what the snapshot recorded (role,
    name, position among siblings, bounds), and released immediately — in a finally, because
    the check runs between taking the reference and giving it back. The reference-counting fake
    in tests/_fakedriver.py fails 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_windows works, through the documented pyjab-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.py fails while
    a pyjab changelog line marked Affects: pyjab-mcp has no entry in acknowledged.yaml, and
    fails for an entry whose marker has gone — so the answer cannot outlive the question.
    tests/contract/test_pyjab_contract.py pins the three shapes this project actually reads
    (the walk, as_record()'s fields, the pump), and .github/workflows/pyjab-drift.yml runs
    the whole suite against pyjab@master weekly on windows-latest, which catches drift
    before a release rather than after. acknowledged.yaml records 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's tools/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 by tests/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 in SURFACE with 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 reading pyjab.__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.md must match what create_server() registers and what is on disk.
      The page gained a module map and a ships column 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, with git check-ignore as the behaviour rather than
      .gitignore as 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 stale build/ copies, and an entry_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, reports READY /
      NOT READY per requirement, and exits non-zero under --require while something is
      missing. UNKNOWN counts as not ready: "I could not measure it" is not "it is there".
  • A guards job and a package job 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 with check_dist_contents.py, and installs the wheel
    into a clean environment to prove the console script reaches real code. build and
    twine are declared in [project.optional-dependencies].dev rather than installed in a
    workflow step, which is how pyjab's own workflows drifted apart once.

  • java_diagnostics now reports a fourth pyjab API fact, non-destructive detach. The
    R2 gap was recorded in prose and then in REQUIREMENTS; 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 the v1.8.0 tag, so JABElement.walk,
    JABTree, pyjab.inspector and Win32Utils.java_process_image_paths all 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 in AGENTS.md §6 and encoded as
    REQUIREMENTS in src/pyjab_mcp/backend.py so 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 called Windows_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 a Future (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...

Read more