<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>UI Automation on Code is cheap, let&#39;s talk</title>
    <link>https://blog.ferstar.org/en/tags/ui-automation/</link>
    <description>Code is cheap, let&#39;s talk</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <copyright>© 2026 ferstar · [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en)</copyright>
    <lastBuildDate>Sun, 18 Oct 2026 18:00:00 +0800</lastBuildDate>
    <ttl>60</ttl><atom:link href="https://blog.ferstar.org/en/tags/ui-automation/index.xml" rel="self" type="application/rss+xml" /><image>
      <url>https://blog.ferstar.org/site-logo.png</url>
      <title>Code is cheap, let&#39;s talk</title>
      <link>https://blog.ferstar.org/</link>
    </image>
    
    <item>
      <title>Driving an Electron Desktop App Through UIA: A Windows-MCP Field Test</title>
      <link>https://blog.ferstar.org/en/posts/ai-agent-drives-electron-app-windows-mcp-uia/</link>
      <pubDate>Sun, 18 Oct 2026 18:00:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/ai-agent-drives-electron-app-windows-mcp-uia/</guid>
      <description>Having an AI acceptance-test an Electron desktop app is awkward: the accessibility tree is nearly empty on a cold start, and window titles, leftover popups and transient UIA errors make replays flaky; this post reads the UIA semantic tree through Windows-MCP, finds windows by process path, narrows every search scope and retries cleanup; after the fixes, 45 consecutive runs had zero failing test steps, about 9 seconds of UI overhead per run excluding the model wait, and SealDesk was not needed anywhere in the chain.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote><p>After <a href="/en/posts/yet-another-remote-desktop-application-sealdesk/" >the last post</a> open-sourced SealDesk, I started wondering what it should gain next. Putting AI control on top of remote desktop sounds like a natural step, but plenty of ready-made options already cover that combination, so instead of changing SealDesk I first ran the existing tools to see whether they were enough.</p>
<p>This time an AI assistant (Claude), working through a remote shell tool, used Windows-MCP on my Windows machine to drive an Electron desktop client: open a new chat, type, send, wait for the reply, assert the result, and finally delete the session the test created. The AI assistant ran the experiments and did the debugging under my direction; I did not track these problems down by hand. The numbers below come from the logs it produced.</p>
<p>The conclusion first: this route works. After a few fixes, 45 consecutive runs had no failing test step, and SealDesk was not used anywhere in the chain.</p>

<h2 class="relative group">The chain
    <div id="the-chain" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-chain" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>The AI assistant only has a remote shell tool and cannot see Windows-MCP’s tool list, so we wrote a small command-line bridge, <code>wmcp.py</code>, with three commands, <code>ping</code>, <code>tools</code> and <code>call</code>, that turns MCP tool calls into shell commands.</p>
<pre class="not-prose mermaid">
flowchart LR
    A["AI assistant"] --> B["Remote shell tool"]
    B --> C["wmcp.py"]
    C --> D["Windows-MCP<br/>127.0.0.1:8000 + bearer key"]
    D --> E["UIA semantic tree / input injection"]
    E --> F["Electron desktop app"]
    H["Person using the machine"] -.->|"USER_CONTROL: yield control"| D
</pre>

<p>Windows-MCP binds to <code>127.0.0.1</code> only and requires a bearer key. Install it with <code>uv tool install</code>, not <code>uvx</code>: uvx keeps its environment in the uv cache, and once the cache is cleaned the resident service can no longer start.</p>
<p>Screenshots exceed the MCP Python SDK’s default 1 MiB limit on a single SSE event, so the bridge uses the lower-level <code>streamable_http_client</code> and turns that limit off. When a person is using the machine, Windows-MCP returns <code>USER_CONTROL</code>; the bridge retries for the wait time it gives and exits on timeout rather than taking control.</p>

<h2 class="relative group">Deal with telemetry before installing
    <div id="deal-with-telemetry-before-installing" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#deal-with-telemetry-before-installing" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>This is based on the 0.8.8 source. The upstream README says usage data is collected; the details are:</p>
<ul>
<li>It is turned off only when the environment variable <code>ANONYMIZED_TELEMETRY</code> equals <code>false</code>; it is on by default.</li>
<li>The client is PostHog, initialized without disabling geoip and with exception autocapture enabled.</li>
<li>Each tool call reports the tool name, duration, success flag, client name and version, plus an anonymous ID persisted under <code>~/.windows-mcp/</code>. The call arguments are not part of that event.</li>
<li>On errors it reports the original error message and a traceback; what they contain depends on the error.</li>
</ul>
<p>The startup script that <code>windows-mcp install</code> generates does not set this variable, so add one line yourself:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bat" data-lang="bat"><span class="line"><span class="cl"><span class="p">@</span><span class="k">echo</span> off
</span></span><span class="line"><span class="cl"><span class="k">setlocal</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="nv">ANONYMIZED_TELEMETRY</span><span class="p">=</span>false
</span></span><span class="line"><span class="cl"><span class="s2">"<uv tool dir>\windows-mcp\Scripts\python.exe"</span> -m windows_mcp serve --transport streamable-http --host 127.0.0.1 --port 8000</span></span></code></pre></div></div>
<p>Windows-MCP also ships tools such as <code>PowerShell</code> and <code>Registry</code>. If the caller already has a shell, they amount to a second path that the outer tool does not govern, and <code>--exclude-tools "PowerShell,Registry"</code> removes them on the server side. I did not remove them this time; we simply agreed not to use them.</p>

<h2 class="relative group">On a cold start, Electron’s accessibility tree is nearly empty
    <div id="on-a-cold-start-electrons-accessibility-tree-is-nearly-empty" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#on-a-cold-start-electrons-accessibility-tree-is-nearly-empty" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Right after the app starts, the first probe returns only 14 nodes: 1 window, 11 Panes and 2 Documents, with no interactive controls at all. Six seconds later the same query returns 764 nodes, 299 of them interactive and 296 with names; buttons, links and toggle states are all readable.</p>
<p>Chromium builds the accessibility tree only when it detects assistive technology, so the first query is itself the wake-up. I had planned to launch the app with <code>--force-renderer-accessibility</code>, but in practice it is not needed: the first step of the replay (preheat) does one full walk of the main window and then polls the node count until it exceeds 200. I fully closed the app and restarted it three times in a row; each time the first preheat reached 402 nodes within about 1.1 seconds.</p>

<h2 class="relative group">The replay case
    <div id="the-replay-case" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-replay-case" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>The case walks the most common path. Every step is located and asserted through UIA properties; a screenshot is saved only at the end, for manual review:</p>
<table>
  <thead>
      <tr>
          <th>Step</th>
          <th>Locator and assertion</th>
          <th>Median time</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>preheat</td>
          <td>One full walk, node count ≥ 200</td>
          <td>203 ms</td>
      </tr>
      <tr>
          <td>new_chat</td>
          <td>Click “New chat”, the input box (Edit) appears</td>
          <td>642 ms</td>
      </tr>
      <tr>
          <td>compose</td>
          <td>Type a probe text with a unique token</td>
          <td>1420 ms</td>
      </tr>
      <tr>
          <td>enable_send</td>
          <td>The “Send” button goes from disabled to enabled</td>
          <td>83 ms</td>
      </tr>
      <tr>
          <td>send</td>
          <td>Click “Send”, recording the earlier reply count</td>
          <td>658 ms</td>
      </tr>
      <tr>
          <td>echo</td>
          <td>Text containing the token appears in the message list</td>
          <td>21 ms</td>
      </tr>
      <tr>
          <td>reply</td>
          <td>A new reply appears: after stripping trailing punctuation it starts with the expected word and is at most 8 characters long</td>
          <td>5283 ms</td>
      </tr>
      <tr>
          <td>cleanup</td>
          <td>Delete only the session that contains this run’s token, with up to two attempts</td>
          <td>5676 ms</td>
      </tr>
  </tbody>
</table>
<p>The enabled state of the “Send” button is one of the few UI states that can be asserted directly. Excluding the model wait, a whole run takes about 9.1 seconds, of which cleanup is 5.7 seconds. The timings in the table come from the last batch of 5 runs.</p>

<h2 class="relative group">Problems you only meet once it runs
    <div id="problems-you-only-meet-once-it-runs" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#problems-you-only-meet-once-it-runs" aria-label="Anchor">#</a>
    </span>
    
</h2>

<h3 class="relative group">The window title changes with the session
    <div id="the-window-title-changes-with-the-session" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-window-title-changes-with-the-session" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>The first version found the main window by name. Once a session had a title, Electron renamed the window to that title and the lookup failed; the earlier runs all showed “New chat”, so it stayed hidden. The fix is to enumerate windows with Win32, match on the process’s executable path, and call <code>ElementFromHandle</code> on those windows:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">app_hwnds</span><span class="p">()</span> <span class="o">-></span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="n">user32</span> <span class="o">=</span> <span class="n">ctypes</span><span class="o">.</span><span class="n">windll</span><span class="o">.</span><span class="n">user32</span>
</span></span><span class="line"><span class="cl">    <span class="n">hits</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nd">@ctypes.WINFUNCTYPE</span><span class="p">(</span><span class="n">ctypes</span><span class="o">.</span><span class="n">c_bool</span><span class="p">,</span> <span class="n">ctypes</span><span class="o">.</span><span class="n">wintypes</span><span class="o">.</span><span class="n">HWND</span><span class="p">,</span> <span class="n">ctypes</span><span class="o">.</span><span class="n">wintypes</span><span class="o">.</span><span class="n">LPARAM</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">visit</span><span class="p">(</span><span class="n">hwnd</span><span class="p">,</span> <span class="n">_</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">user32</span><span class="o">.</span><span class="n">IsWindowVisible</span><span class="p">(</span><span class="n">hwnd</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="n">pid</span> <span class="o">=</span> <span class="n">ctypes</span><span class="o">.</span><span class="n">wintypes</span><span class="o">.</span><span class="n">DWORD</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">            <span class="n">user32</span><span class="o">.</span><span class="n">GetWindowThreadProcessId</span><span class="p">(</span><span class="n">hwnd</span><span class="p">,</span> <span class="n">ctypes</span><span class="o">.</span><span class="n">byref</span><span class="p">(</span><span class="n">pid</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">            <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="n">APP_EXE_MARK</span> <span class="ow">in</span> <span class="n">psutil</span><span class="o">.</span><span class="n">Process</span><span class="p">(</span><span class="n">pid</span><span class="o">.</span><span class="n">value</span><span class="p">)</span><span class="o">.</span><span class="n">exe</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">                    <span class="n">hits</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">hwnd</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">except</span> <span class="p">(</span><span class="n">psutil</span><span class="o">.</span><span class="n">Error</span><span class="p">,</span> <span class="ne">OSError</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">                <span class="k">pass</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">user32</span><span class="o">.</span><span class="n">EnumWindows</span><span class="p">(</span><span class="n">visit</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">hits</span></span></span></code></pre></div></div>
<p>This is an excerpt with the names changed.</p>

<h3 class="relative group">Don’t walk every window on the desktop
    <div id="dont-walk-every-window-on-the-desktop" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#dont-walk-every-window-on-the-desktop" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>The earliest version looked for top-level windows by going down from the UIA root. One run stalled for 63 seconds, and in two others the root query returned 0 windows. I suspect it hit an unresponsive window but did not verify that. After switching to touching only the target app’s own windows, the problem did not recur.</p>
<p>The same idea fixed the slow cleanup. The menu and the confirmation dialog are popup windows of the target app, but I first searched for them across the whole desktop: scanning 9 top-level windows took 5.3 seconds, and limiting the search to the app’s own windows took 0.02 seconds. The median cleanup time dropped from 29.6 seconds to 5.7 seconds. Most likely the large trees of other applications (browsers and the like) were slowing it down; I did not measure window by window.</p>

<h3 class="relative group">A menu left open leaves the next run with a 20-node tree
    <div id="a-menu-left-open-leaves-the-next-run-with-a-20-node-tree" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#a-menu-left-open-leaves-the-next-run-with-a-20-node-tree" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>In one run the cleanup failed and the “session actions” dropdown stayed on screen. The next run’s preheat waited 30 seconds and saw an accessibility tree of only 20 nodes, and the run after it did the same. Now Esc is pressed once before each run and after a failed cleanup.</p>

<h3 class="relative group">Clicking a menu item as soon as it appears doesn’t register
    <div id="clicking-a-menu-item-as-soon-as-it-appears-doesnt-register" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#clicking-a-menu-item-as-soon-as-it-appears-doesnt-register" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Cleanup failed in two ways: the click on “Delete” happened but no confirmation dialog appeared, or the dialog appeared but its confirm button could not be found. My judgment is that the menu was still animating open and the early click got swallowed. Waiting 0.6 seconds after the item appears, then locating and clicking it again, cut first-attempt cleanup failures from 4 in 20 runs to 2. They did not go away, so I added one retry; in later batches no run failed both attempts. I have not fully pinned down the root cause.</p>

<h3 class="relative group">Transient UIA COM errors
    <div id="transient-uia-com-errors" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#transient-uia-com-errors" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>In the first batch of 20 runs, 3 runs raised <code>COMError -2147220991</code> (<code>EVENT_E_ALL_SUBSCRIBERS_FAILED</code>) while waiting for the reply. After adding up to 3 retries to UIA queries, it did not appear again in the next 45 runs.</p>

<h3 class="relative group">Coordinates must be in physical pixels
    <div id="coordinates-must-be-in-physical-pixels" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#coordinates-must-be-in-physical-pixels" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>This machine runs at 200% scaling. If the process does not declare DPI awareness, UIA returns logical coordinates, which are half the physical coordinates used for input injection, and clicks land in the wrong place. Calling <code>SetProcessDpiAwarenessContext(-4)</code> (per-monitor v2) at the top of the script makes the two agree.</p>

<h2 class="relative group">Test results
    <div id="test-results" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#test-results" aria-label="Anchor">#</a>
    </span>
    
</h2>
<table>
  <thead>
      <tr>
          <th>Batch</th>
          <th>Runs</th>
          <th>Test steps passed</th>
          <th>Cleanup eventually succeeded</th>
          <th>Change</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>A</td>
          <td>20</td>
          <td>17</td>
          <td>16</td>
          <td>First version; the 3 failing runs were the COM error above</td>
      </tr>
      <tr>
          <td>B</td>
          <td>20</td>
          <td>20</td>
          <td>16</td>
          <td>Retries added to UIA queries</td>
      </tr>
      <tr>
          <td>C</td>
          <td>20</td>
          <td>20</td>
          <td>20</td>
          <td>0.6-second wait for the menu item, up to two cleanup attempts</td>
      </tr>
      <tr>
          <td>D</td>
          <td>5</td>
          <td>5</td>
          <td>5</td>
          <td>Popups searched only within the app’s own windows</td>
      </tr>
      <tr>
          <td>Cold start</td>
          <td>3</td>
          <td>3</td>
          <td>3</td>
          <td>App fully closed and relaunched before each run</td>
      </tr>
  </tbody>
</table>
<p>Across B to D, 45 runs in total, no test step failed. A few scattered runs during debugging are not counted.</p>

<h2 class="relative group">What this asks of the front end
    <div id="what-this-asks-of-the-front-end" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#what-this-asks-of-the-front-end" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>The replay works because UIA can find things by name and control type. A few things are worth fixing in the front end:</p>
<ul>
<li>Only 11 of 764 elements carry an AutomationId. Values such as <code>root</code> and <code>DndLiveRegion-0</code> show that an HTML <code>id</code> is mapped to AutomationId; but the existing ones are mostly framework-generated (for example React’s <code>radix-_r_9_</code>), depend on render order, and are not suitable as selectors. Key controls could get stable <code>id</code> values.</li>
<li>Every row in the session list has “Rename” and “Delete” buttons with the same names, 60 elements each; the buttons’ <code>aria-label</code> needs to include the session name to tell rows apart.</li>
<li>The same input box is an Edit in an empty chat and changes control type once a conversation is open, so the locator can only try several types in turn.</li>
</ul>

<h2 class="relative group">What this means for SealDesk
    <div id="what-this-means-for-sealdesk" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#what-this-means-for-sealdesk" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Looking back over the whole exercise, SealDesk was not used for a single line of code. The AI assistant reached this machine through an existing remote shell channel, it operated through the UIA semantic tree rather than pixels, and yielding control when a person is using the machine is built into Windows-MCP.</p>
<p>Where SealDesk can add something is where the semantic tree cannot reach: custom-drawn interfaces, games and CAD, desktops inside remote virtual machines, and the moments when a person needs to watch the actual picture and take over with low latency; also the connection leg itself when the machine has no ready-made channel. How common those cases are, and whether they justify adding agent capabilities to SealDesk, I don’t have an answer yet.</p>

<h2 class="relative group">Limitations
    <div id="limitations" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#limitations" aria-label="Anchor">#</a>
    </span>
    
</h2>
<ul>
<li>Every run creates a session in the account and makes one model call; the session is deleted when the test ends.</li>
<li>Only one app and one flow were tested.</li>
<li>It was not compared with a screenshot-based approach, so I can only say the semantic tree is sufficient, not that it is faster or more stable.</li>
<li>The UI strings are hard-coded Chinese literals and need updating whenever the interface changes.</li>
<li>The reply assertion only checks that the reply starts with the expected word and is at most 8 characters long; if the model phrases it differently, the run is judged a failure.</li>
<li>When the desktop session is disconnected (an RDP disconnect, for example), Windows-MCP returns <code>CONTROL_UNAVAILABLE</code> and the script fails outright instead of reporting a false pass.</li>
</ul>
<p>The replay script is tied to a specific app’s interface, so only the generic parts are shown here. Windows-MCP is available on <a href="https://github.com/CursorTouch/Windows-MCP"  target="_blank" rel="noreferrer">GitHub</a> and <a href="https://pypi.org/project/windows-mcp/"  target="_blank" rel="noreferrer">PyPI</a>.</p>
]]></content:encoded>
      
    </item>
    
  </channel>
</rss>
