ResearchMesh-Windows, a Windows CLI Assistant/Research Client for use with Anthropic API, but can be subsidized as an agent for Claude Code/Desktop
Unofficial, community-built client — not affiliated with or endorsed by Anthropic. "Claude" is a trademark of Anthropic.
Note
Windows only. main.py and mcp_server.py refuse to start on anything else rather
than half-working. A fork of ResearchMesh at
commit 9e6959b, rewritten for Windows. ruff, mypy and smoke_test.py pass, CI runs
on windows-latest, and it has since been driven hands-on on a real Windows box too.
computer's biggest caveat — it cannot click into an elevated window — has been
confirmed directly this way (see Good to know below); document_convert and
interactive_run are the two still most worth double-checking in your own environment.
┌── /think
├── /clear
│
├── PowerShell
├── Filesystem
├── LibreOffice
ResearchMesh-Windows ──┼── Playwright
├── MCP #1
├── MCP #2
├── MCP #3
└── ...
A terminal chat client for the Anthropic API that hands Claude real tools on your own Windows
machine: a shell, a file editor, a headless browser it can surf with, a persistent Python
session, desktop control, and document conversion. Ask it something and it can look it up,
read the pages, run the commands, and hand you back a finished .docx — in one conversation.
It works in both directions: it connects out to your own MCP servers, and it can itself be added to Claude Code as one, so Claude Code can hand it the jobs it can't do — see below.
19 local tools, plus whatever your MCP servers expose:
| Tool | For |
|---|---|
powershell |
PowerShell commands as your user. Stateless — fresh process each call |
str_replace_based_edit_tool |
View, create, and edit files. Preserves each file's existing line endings |
web_search · web_fetch |
Anthropic's server-side search and page fetch |
memory |
A /memories store that persists across sessions — the only state that outlives the process |
computer |
Screenshots plus mouse/keyboard control of your desktop (caveats) |
browser_navigate · _links · _click · _fill · _extract · _back |
Headless Playwright — real DOM surfing: renders JavaScript, follows links, fills forms |
document_convert |
LibreOffice + pandoc. Markdown → .docx/.odt/.pdf, or any office format to any other |
python |
Persistent IPython kernel — variables survive between calls |
interactive_run |
Commands that prompt: passwords, [y/N], ssh host keys, winget and other installers, REPLs |
config_edit |
Edit YAML/TOML/JSON without destroying your comments |
sql_query |
DuckDB straight against CSV/Parquet/JSON — no import step |
trash |
Recoverable deletes to the Recycle Bin. Remove-Item bypasses it entirely and has no switch to use it, so this is the only undo you get |
text_embeddings |
Vector embeddings from an HTTP embedding server you configure — self-hosted or a paid API both work. See [embeddings] in config.toml for worked examples |
Claude chooses the tools and keeps working until it has an answer.
- There is no approval prompt. Claude runs the commands and file edits it decides on, as
your user, with no y/n in between. Built for local development.
trashexists so deletes are at least recoverable. - It's your API key: one request can fan out into many tool calls (capped at 30 per turn).
powershellforgets everything between calls —cd,$env:changes, activated venvs. Chain with;in one call, or usepython, which keeps state.- Ask for files by absolute path. If Claude offers a download link instead, tell it you need the file written to disk.
- Network shares and most removable drives have no Recycle Bin, so deletes there would be
permanent —
trashsays so rather than pretending. computercannot touch an elevated window. Windows blocks input from a normal process into one running as Administrator (UIPI), so if an elevated terminal, a UAC dialog, Task Manager or some installer has focus, clicks and keystrokes are silently discarded and the screenshot afterwards looks like a click that missed. Typing reports it; clicking cannot. If a sequence has no visible effect, suspect this first. Every other tool is unaffected. Confirmed directly: an ordinary window (e.g. Notepad) takes clicks and menu navigation immediately, whilemmc.exe(Certificates snap-in, Group Policy, and friends) silently swallows every click the moment Windows has quietly elevated it — which it will, even from a non-admin shell, with no visible UAC prompt on some machines. IfUACis off for your account or set to auto-elevate, expect to hit this. Running the whole client elevated fixes it for elevated targets, at the obvious cost of also giving it admin.computeris primary-monitor only and assumes the client is DPI-aware, which it sets at startup. A second monitor is not captured.- If Sonnet gets inconsistent on a complicated multi-tool request, set
modelto an Opus one. - Optional packages are imported only when a tool is used, so a missing one breaks just that tool and tells you what to install.
- If a tool reports a missing package that
requirements.txtalready lists (e.g.sql_query'sduckdb, orconfig_edit'sruamel.yaml/jsonpath-ng), that's not a docs gap — your venv just predates that line. Everything inrequirements.txtis a>=floor rather than a pin (there's no lockfile), so a venv can satisfy it and still miss a package added later. Re-runpip install -r requirements.txt; you don't need to restart the app, because each optional package is imported at the moment its tool is called. - Linting: one linter is configured,
ruff, andruff check .should pass.pyproject.tomlhas a[tool.ruff.lint]section. It adds no rules — it only switches three off, each with its reason written next to it, so a clean run is the expected baseline and any finding you do see is genuinely new: your own code, or a rule a newer ruff added. (The rule selection is left at ruff's defaults, which do shift between versions.) Ruff is not a dependency and nothing runs it for you — install it yourself if you want it. There's no[tool.black]and no.pylintrc. - Type checking:
mypy .should pass too.pyproject.tomlhas a[tool.mypy]section setting exactly one option (ignore_missing_imports, because the optional tool backings are lazily imported and legitimately absent from a bare venv); strictness stays at mypy's defaults, so unannotated function bodies aren't checked. It's worth having here because mypy checks against the packages you actually have installed, which makes it the gate that catches a dependency changing shape under you — it named every mcp 1.x → 2.x rename in one run, including the ones incore/tools.pythat the smoke test can't reach. python smoke_test.pybefore you commit. Seconds, no API key, no network, no optional packages. It checks that everything imports, that the tool registry is well-formed, that the tool count in the docs still matches the code, and thatmcp_server.pycompletes an MCP handshake. GitHub Actions runs it plusruffandmypyon every push and PR tomain(.github/workflows/ci.yml), on Python 3.11 and 3.14.- There are still no unit tests, and CI deliberately doesn't exercise the tools themselves
— that would need LibreOffice, a browser, a real desktop and real API credits. If your venv
happens to have
pylint/blackinstalled (neither is a project dependency) they're safe to run by hand — expect plenty of output, since nothing is configured for them. - Two things a linter will fight you on here — worth knowing before you "fix" them.
Broad
except Exception/except BaseExceptionis the design, not sloppiness: every local tool must catch anything and return an error string rather than crash the chat loop, which is whyBLE001is switched off project-wide. And cleanup paths (shutdown,close) must not be able to fail or fail silently — narrowing one has already caused a real bug, sincezmq.ZMQErrorisn't anOSErrorand escapingshutdown()turns an ordinary Ctrl-C into a traceback. Blanket catch plus aprint()is the pattern. - ** Using it **
For extended thinking, type.
/think <message>gives Claude longer to reason on hard problems;/cleardrops the conversation without restarting the app; Ctrl-C exits and shuts everything down cleanly.
python 3.14, powershell 7.x and node 24.16 LTS or newer 24.x LTS is required
- setup powershell 7 as default powershell, do NOT use legacy 5.x
- https://learn.microsoft.com/en-us/powershell/scripting/install/install-powershell-on-windows?view=powershell-7.6
- if there is a newer version than 7.6, then look it up in a browser for the newest version link rather than what I have above
- add a shortcut for powershell 7 to the taskbar, be SURE you select the correct item from Microsoft's start/window menu
- if you want to verify yourself, then it probably installed it at:
C:\Program Files\PowerShell\7\pwsh.exe - you can go straight to the folder the file is in:
C:\Program Files\PowerShell\7\, and right click on pwsh.exe, and Pin to start or add to favorites, whichever you like better - right click and launch python 7 as admin
- type:
wsl --install - exit powershell admin session
2) setup npm on windows: https://learn.microsoft.com/en-us/windows/dev-environment/javascript/nodejs-on-windows
- download npm-setup.zip (scroll past the antivirus links for checking files) Link below: https://github.com/nvm-windows/nvm/releases
- unzip, and run as regular user.
- check all options
- install npm
- open powershell as regular user
- type:
nvm install 24.16(or newer LTS only) - type:
nvm list - type:
nvm use(whatever newer LTS version, revert to 24.16 if you have issues with newer versions. Stay away from bleeding edge. I tested with 24.16) - type:
node -v - type:
npm -v - exit powershell
- https://docs.python.org/3/using/windows.html
- use the microsoft store installer, install version 3.14 newest version
- check all options, if any
- go to settings -> apps -> advanced app settings -> app execution aliases
- enable python python.exe (default) # "in the GUI settings"
- enable python python3.exe (default) # "in the GUI settings"
- enable Python install manager pymanager.exe # "in the GUI settings"
- enable Python install manager py.exe # in the "do you understand now? wth"
- enable Python install manager (windowed) pywmanager.exe # ditto
- enable Python install manager (windowed) pyw.exe # ditto
5) setup your Anthropic API key and the following for your windows environment (so you don't have to put the key in ReserachMesh itself)
- If you use Claude Desktop/Claude Code (node cli) and have a subscription, you will also want an alias so it doesn't use your API key
- you will have to create a function in your powershell profile, but keep reading before you go copy/pasting.
function claude {
$oldKey = $env:ANTHROPIC_API_KEY
Remove-Item Env:ANTHROPIC_API_KEY -ErrorAction SilentlyContinue
try {
& claude.exe @args
}
finally {
$env:ANTHROPIC_API_KEY = $oldKey
}
}- BUT FIRST, check whether you have a profile.
- Go here and get an API key and throw 20 bucks at it to test with: https://platform.claude.com/dashboard
- type in powershell as user:
$PROFILE - then launch your code editor. notepad++ or VS-Code will work fine.
- the output of
$PROFILEwill produce a path to a file for the profile, but it doesn't mean the folder OR file exists if you have never had to have it before - if you don't have that folder and file from the output of
$PROFILE, create the folder/file if needed, or open the existing file, then open it up notepad++ - TIP: If you don't have a claude code subscription and don't plan on one, then don't add the alias function.
- type or copy/paste the following at the bottom of the profile file:
# ============================================================
# Anthropic / Claude Configuration
# ============================================================
#
# ANTHROPIC_API_KEY
# Available to third-party agents and other applications
# that use the Anthropic API.
#
# CLAUDE_KERNEL_ENCRYPTION
# Requires Claude Kernel encryption.
#
# Claude Code
# The `claude` function below temporarily removes
# ANTHROPIC_API_KEY from Claude Code's environment.
#
# This mirrors the Linux setup:
#
# alias claude='env -u ANTHROPIC_API_KEY claude'
#
# The API key is restored to this PowerShell session
# after Claude Code exits.
# ============================================================
# Make the Anthropic API key available to programs launched
# from this PowerShell session.
$env:ANTHROPIC_API_KEY = "YOUR_API_KEY_HERE"
# Require Claude Kernel encryption.
$env:CLAUDE_KERNEL_ENCRYPTION = "required"
# ============================================================
# Claude Code launcher
# ============================================================
function claude {
# Save the API key currently in this PowerShell session.
$savedKey = $env:ANTHROPIC_API_KEY
# Remove it from Claude Code's environment.
Remove-Item Env:ANTHROPIC_API_KEY -ErrorAction SilentlyContinue
try {
# Start Claude Code and pass through any arguments.
& claude.exe @args
}
finally {
# Restore the API key after Claude Code exits.
if ($null -ne $savedKey) {
$env:ANTHROPIC_API_KEY = $savedKey
}
}
}- save the file
6) open powershell as regular user, check to make sure you launched powershell 7 by typing: $PSVersionTable.PSVersion
- create python sandbox, type:
python -m venv pvenv - type:
C:\Users\YOURUSERNAME\pvenv\Scripts\Activate.ps1# or whatever the path is to your pvenv enviroment - change directory to wherever you want to keep the clone of ReserachMesh-Windows
- Install git in powershell 7. Open up Powershell 7 as a regular user, type:
winget install --id Git.Git -e --source wingetOR if you prefer the alternative post-git moduleInstall-Module posh-git -Scope CurrentUser -Force - type:
git clone -h# to see all the git clone options if you did a full install of git on your windows box, not covered in this repo - type:
git clone https://github.com/nodormu/ResearchMesh-Windows - type:
cd ResearchMesh-Windows - type:
pip install -r requirements# hopefully you don't get any errors, conflicts or wheel issues, if so then just chatgpt/claude/glm that issue for a fix, just be careful about it leading you down rabbit holes of "you must have done this", or "or lets check for sure" etc etc etc and try again. AIs will feed you BS. Be VERY specific with memories and context before ANY prompt/request. - type:
playwright install chromium
winget install TheDocumentFoundation.LibreOffice # for document_convert, or leave this out if use 365
winget install JohnMacFarlane.Pandoc # for document_convert, for libreoffice and/or 3657) exit powershell in case and restart as regular user just to make it easier instead of establishing environment variables at the CLI
- type:
cd C:\Users\YOUR USER NAME\path\to\ResearchMesh-Windows - type:
C:\Users\YOUR USERNAME\pvenv\Scripts\Activate.ps1# or whatever your python sandbox and $PROFILE file name is if its not Activate.ps1 - to start the CLI Assistant, type:
python .\main.py - to start the MCP server, type:
python .\mcp_server.py - if you want to utilize ResearchMesh as an MCP Client for usage with the CLI assistant AND/OR MCP server, edit the config.toml for your environment and run either or both above commands and see if you can see the tools in your connected MCP servers. You can have 2 different powershell sessions open so you can run ResearchMesh as an MCP and use the CLI assistant at the same time.
- if you want to test connection from ResearchMesh-Windows to any MCP servers, then type:
python .\mcp_clientBe sure you have node installed for the user the Agent is running on. I advise against putting the ResearchMesh-Windows Agent on under the root Admin user, and just use your regular user account.
Now you have a second "you" on your computer as your user. Think about it. Do you ask yourself for approval when you need to open a document or surf to a website? No, you just do it. That's the whole point of ResearchMesh. Its YOU on your computer, allowing you to talk naturally to it while it handles the technical details, however; proper context with prompts can help AI responses zero in on your request. You can change the Anthropic model in the config.toml file also. Have fun. Be safe with this.
To utilize CLI Assistant, go to your python pvenv and type
python main.py
To expose it as an MCP server for Claude Code or whatever you want to access it with, type
python mcp_server
If you want to see SSL options, type
python mcp_server --help
Need to test you MCP servers that are connected to ResearchMesh? (requires node), type
python mcp_client and of course python mcp_client --help for help file
Here are some tests you can try at the CLI assistant once you throw 20 bucks at Anthropic API to test with. a) Export the first certificate found in certmgr.exe with powershell and put it on my desktop b) Open Edge and look up the latest news headlines for the United States. c) Go to news.ycominator.com via DOM based surfing, open the top #1 article, and summarize it. NOTE: This is an example of AI surfing the net without loading a visible browser or controlling your GUI. d) Open the link for the article you just summarized so I can see it in my default browser. e) Open Notepad and type, "Hello, I am controlling your mouse and keyboard.", save it to desktop, then export as PDF, also saved to the desktop. TIP: Don't use the mouse and keyboard while its trying to, as this just makes it difficult for the AI. f) Give me a Cisco IOS 17.15 config for a 9200 24-port switch. Set VTP to client so my VLAN database isn't overwritten on the network its connecting to with two uplinks active/standby at 1 Gbps, all 24 ports up and ready for voice + data VLANs to be pushed from the VLAN server with the uplink trunk on VLAN 100. Note what I need to change for my environment, then write it to my Desktop in a plain text file.
If it can't do something controlling your mouse and keyboard, it can probably do it with powershell if your user in powershell can do it. UAC may cause you headache getting this to function, so if you have UAC on, it's not much help for you as a project to use a duplicate you. Also, Windows UIPI wil invisibly block synthetic input from a Medium-integrity source into a High-integrity window.
Non-secret settings live in config.toml. Secrets stay in the environment — the app does
not read a .env file.
[claude]
model = "claude-sonnet-5" # CLAUDE_MODEL overrides this
[mcp]
enabled = true # false skips every server; local tools still work
# One line per server. Add as many as you like — every reachable/launchable one
# connects and its tools join the same list Claude sees. Two entry shapes:
#
# Streamable HTTP (a server already running elsewhere):
# url the server's endpoint
# token_env names the environment variable holding that server's bearer
# token; omit it if the server needs none
#
# stdio (a local server main.py launches itself, no separate process to start
# by hand — it talks JSON-RPC over the subprocess's stdin/stdout):
# command full argv as a list, e.g. ["node", "C:/path/to/bin.js"]
# env optional table of extra environment variables for it
servers = [
{ name = "n8n", url = "http://192.168.2.12:5678/mcp-server/http", token_env = "N8N_MCP_TOKEN" },
{ name = "alpaca", url = "http://192.168.2.12:8000/mcp" },
{ name = "unreal", command = ["node", "%USERPROFILE%/unreal-mcp/dist/bin.js"] },
]A server that's unreachable (http) or fails to launch (stdio) prints a warning and is skipped, so one being down doesn't stop the app. Tokens are never written in this file — only the name of the variable that holds them.
~, %USERPROFILE%, %USERNAME% and any other %VAR% are expanded in command, url and
the values of env, so the checked-in config doesn't have to name your user account.
$VAR and ${VAR} work too, but use the Windows names: there is no $HOME or $USER
here, and a path copied from a config written for another OS passes through unexpanded.
(env's keys are variable names and are left alone.) An undefined variable is left as
written rather than expanding to nothing, so a typo shows up in the startup warning instead
of becoming a silently wrong path.
Backslashes need care in TOML: "C:\Users\me" is invalid, because \ starts an escape
inside a basic string. Use forward slashes (every Windows API accepts them) or a literal
single-quoted string, 'C:\Users\me'. Absolute paths beyond that are machine-specific —
those you edit by hand.
| Variable | Purpose |
|---|---|
ANTHROPIC_API_KEY |
Required |
| (per server) | Whatever each token_env names, e.g. N8N_MCP_TOKEN |
RESEARCHMESH_MCP_TOKEN |
Bearer token clients must present to mcp_server.py --transport streamable-http; unset = no auth |
CLAUDE_MODEL |
Override the model |
CLAUDE_SHOW_USAGE=1 |
Print token and prompt-cache counts per request |
CLAUDE_MEMORY_DIR |
Where memory stores /memories (default ./memories) |
CLAUDE_DISPLAY_SIZE |
Logical screen size computer reports, e.g. 1280x800 |
CLAUDE_KERNEL_ENCRYPTION |
auto (default) encrypts the python kernel's sockets with CurveZMQ and falls back if it can't; required fails the tool instead of running unencrypted; off skips it |
| (embeddings server) | Whatever [embeddings].api_key_env names, if your server needs auth |
ResearchMesh is a client and a server at the same time. The two are independent — use either, both, or neither:
Claude Code ──delegate──▶ ResearchMesh ──▶ n8n / Unreal / Unity / …
(any MCP client) (server AND client) (its own MCP servers)
│ │ │
mcp_server.py 19 local tools [mcp] in config.toml
As a client, it connects out to MCP servers and merges their tools with its own — that's
[mcp] in Configuration above. As a server, it hands another client the
whole agent as one delegate tool, so Claude Code can offload what it structurally can't do
itself: drive GUI apps, answer password / [y/N] prompts, keep a live Python kernel between
steps, surf a real DOM, and reach ResearchMesh's own MCP servers.
claude mcp add researchmesh --scope user `
--env ANTHROPIC_API_KEY="$env:ANTHROPIC_API_KEY" `
-- "$HOME\researchmesh\Scripts\python.exe" C:\path\to\ResearchMesh-Windows\mcp_server.pyIf you put your key in your $PROFILE then you don't have to set the env as shown above.
That's it — no token, no ports, nothing to start. Claude Code launches the server itself when it needs it. Then just ask it to delegate something: "use researchmesh to take a screenshot and tell me what window is focused."
Two ways it fails, both at the first call:
ANTHROPIC_API_KEYnot set — a client passes stdio servers only a small safe subset of the environment, so exporting it in your shell isn't enough. That's what--envabove is for. The server says so at startup rather than failing cryptically later.- Wrong python — use the venv interpreter that has the dependencies, not bare
python. The client spawns this with noPATHof yours and no activated venv.
A .mcp.json ships in the repo as a working equivalent if you'd rather commit the config than
run the command.
Streamable HTTP — for clients that connect to an already-running endpoint
stdio (above) is right whenever the client launches its own server — Claude Code, Claude Desktop, most editors. Use HTTP instead to share one agent between several clients, or for a client that only speaks HTTP:
python mcp_server.py --transport streamable-http --port 8765
# point the client at http://127.0.0.1:8765/mcp--host defaults to 127.0.0.1, reachable only from this machine. --path, --port and
--json-response are there too (--json-response returns one JSON body instead of an SSE
stream).
Auth is the token_env arrangement from config.toml, pointed the other way. Set the
variable and it's required; leave it unset and the endpoint is unauthenticated, which is
allowed by design and announced at startup:
$env:RESEARCHMESH_MCP_TOKEN = "<token>" # see Tokens below
python mcp_server.py --transport streamable-http --host 0.0.0.0Clients send Authorization: Bearer <token> — exactly what a token_env entry produces, so
another ResearchMesh consumes this one with a plain config.toml line. Same token, same
variable name, set on both machines:
{ name = "desktop", url = "http://192.168.2.5:8765/mcp", token_env = "RESEARCHMESH_MCP_TOKEN" }Unauthenticated and bound off-loopback prints a warning, because at that point anyone who
can reach the port has unrestricted shell and desktop control of the machine. The token is
read from the environment, never passed as an argument, so it stays out of ps and shell
history. --token-env VAR renames the variable.
TLS is a pair of paths, not a mode. Without them the endpoint is plain HTTP — the bearer token and every task and result cross the network in the clear, which is called out at startup on a non-loopback bind:
python mcp_server.py --transport streamable-http --host 0.0.0.0 `
--ssl-certfile C:\certs\worker-fullchain.pem `
--ssl-keyfile C:\certs\worker.keyThe startup line then says https://. Give --ssl-certfile the full chain — leaf first,
then intermediates — which is what a company CA or a public issuer hands you; a leaf-only file
verifies on the box that has the intermediate cached and fails everywhere else. The two must be
given together (uvicorn quietly serves plain HTTP with only one, so this refuses instead), and
both paths are checked to exist before the port opens.
Nothing is configured on the client side to match: the URL becomes https://… and verification
goes through the connecting machine's own OS trust store, so a company CA already rolled out to
that machine is trusted, as is any public certificate. $env:SSL_CERT_FILE overrides that per
process if you'd rather not import a CA into the machine store.
Both transports are the same server object — no separate build, no high-level-server rewrite. Under HTTP the stdout guard is skipped (fd 1 isn't the wire there) so the app's messages become ordinary service logs, line-buffered so a redirected log fills in live rather than on exit. Running the stdio form by hand just waits on stdin, which is a healthy stdio server behaving normally.
Tokens — generating one, and where it actually has to live
Only needed for --transport streamable-http. Under stdio there's no port and nothing to
authenticate.
Generate one with the interpreter this project already requires — no openssl needed:
python -c "import secrets; print(secrets.token_urlsafe(32))"256 bits from the OS CSPRNG. There's deliberately no generate_token.py here: a file wrapping
one line of stdlib would be the same mistake as a tool wrapping a command PowerShell could already
run.
The value lives in an environment variable; only its name goes in a file. Which file depends on how the process starts, and this is the part that catches people:
| How it starts | Where the token has to be |
|---|---|
| You, from an interactive shell | setx RESEARCHMESH_MCP_TOKEN … (persists; new shells only) plus $env:RESEARCHMESH_MCP_TOKEN = … for the current one |
| A Windows service or Scheduled Task | Set it machine-wide or in the service's own environment — neither inherits your interactive shell |
| Spawned by an MCP client | the env block of that server's entry in the client config |
Two things to get right:
- Never put the literal token in a committed file.
.mcp.jsonandconfig.tomlare both in git — use${RESEARCHMESH_MCP_TOKEN}andtoken_envrespectively. - One name is normally right. It's one token, and each end reads the variable from its own
environment, so both machines can call it
RESEARCHMESH_MCP_TOKEN. You only need a second name if a single machine both serves an endpoint and consumes someone else's — then one variable would have to mean two different secrets. Rename either end with--token-env VARortoken_env = "VAR".
Can you just ask ResearchMesh to set it up? Mostly. It can generate the token, persist it
with setx, and update a consuming config.toml. It cannot set the variable in the shell
you are sitting in — the powershell tool is a fresh process per call, and a child cannot
alter its parent's environment anyway — so you still need a new shell and a server restart.
Tell it not to write the literal token into anything in the repo.
Full setup detail — OS libraries, document tools, which package backs which tool
Playwright. pip installs the Python package but not the browser itself:
playwright install chromium # the browser binaryplaywright install with no browser name fetches all three engines; this app only launches
Chromium, so the argument is worth keeping. There is no install-deps step — that installs
shared libraries for other operating systems and does not apply here.
Document conversion. soffice (LibreOffice) handles docx/odt/xlsx/pptx/html/rtf/txt and
PDF output, each call in a throwaway user profile so two conversions can't collide on the
profile lock. pandoc handles markdown, because soffice has no dependable markdown
import; md → pdf goes through odt on the way, since pandoc's own PDF writer would need a
LaTeX engine. libreoffice-writer/-calc/-impress alone are enough if you don't want the
whole suite.
Computer use needs no system packages. pyautogui and pillow from
requirements.txt are the whole dependency and screen capture works out of the box.
Two limits worth knowing before you rely on it:
- Elevated windows are unreachable. Windows blocks input from a normal-integrity process
into a window owned by an elevated one (User Interface Privilege Isolation). If an
Administrator terminal, a UAC consent dialog, Task Manager or an installer has focus,
clicks and keystrokes are discarded — and the screenshot afterwards looks exactly like a
click that missed.
typedetects it (SendInputreports how many events landed) and says so; the mouse actions get no such signal from Windows and fail silently. Running the whole client elevated fixes it for elevated targets, at the obvious cost. - Primary monitor only. Capture is deliberately limited to the primary display: the coordinate maths scales against that screen's size, so grabbing the whole virtual desktop would put every click on the wrong monitor. Proper multi-monitor support needs the virtual desktop's bounds and origin, which can be negative.
The client declares per-monitor DPI awareness at startup. Without it Windows reports virtualised coordinates on a scaled display while screenshots come back at physical resolution, and every click drifts further off toward the bottom-right.
The tool reports a fixed logical screen size (CLAUDE_DISPLAY_SIZE, default 1280x800)
and downscales every screenshot to exactly that, scaling Claude's coordinates back up to
your real resolution. That's what keeps clicks landing where Claude aims — the declared
size and the image it sees can never drift apart. Below roughly 1280x720, accuracy drops.
Memory writes to ./memories by default (CLAUDE_MEMORY_DIR to relocate). Claude sees
it as /memories; every command is confined to that directory, so a traversal path like
/memories/../../.ssh/id_rsa is rejected rather than served. It's a private scratchpad for
Claude, not a place for your project files — and it persists until you delete it.
Optional Python packages (all in requirements.txt; each is imported lazily):
| Tool | Needs |
|---|---|
python |
jupyter_client>=8.9.1, ipykernel>=7 — older versions work, but unencrypted (see below) |
interactive_run |
pywinpty (ConPTY) |
config_edit |
ruamel.yaml (YAML), tomlkit (TOML), jsonpath-ng ($… queries); JSON needs nothing |
sql_query |
duckdb |
trash |
send2trash |
computer |
pyautogui, pillow |
memory |
nothing — standard library only |
To drop a tool entirely, remove its module from MODULES in core/local_tools.py.
The python kernel's sockets are encrypted. Everything that tool does — your code, your
data, the results — travels over ZeroMQ, which by default is plaintext on four loopback TCP
ports; ipykernel says so itself, warning on every start that the link "is susceptible to
eavesdropping". ResearchMesh has the kernel manager provision a CurveZMQ keypair instead, so
both ends talk CURVE. That needs jupyter_client>=8.9.1 and ipykernel>=7 (and a pyzmq built
with libsodium, which the wheels are); on anything older it falls back to plaintext TCP,
printing why. There is no middle tier — ZeroMQ's ipc:// transport has no Windows
implementation — so CurveZMQ is the only thing between that link and an open loopback port. Set CLAUDE_KERNEL_ENCRYPTION=required to make an unencrypted kernel a hard error
rather than a fallback — if you see that error, pip install -U 'jupyter_client>=8.9.1' 'ipykernel>=7' is the fix.
Environment variables must be set for the user account you launch as — main.py calls
os.getenv() directly. setx VAR "value" writes them to the user environment but only
affects shells started afterwards, so set $env:VAR = "value" as well for the shell you are
in. Then check without revealing anything:
# Prints True/False without echoing the value.
[bool]$env:ANTHROPIC_API_KEY, [bool]$env:N8N_MCP_TOKENTargets Windows 10/11 and Python 3.11+ (pyproject.toml; the floor is tomllib, used by
main.py). CI runs 3.11 and 3.14 on windows-latest.
HTTPS and TLS — for an MCP server with a self-signed or private-CA certificate
A server URL may be http:// or https://. TLS is verified by the httpx2 client inside
mcp_client.py, offline — the CA is not contacted at connect time.
Verification goes through the Windows certificate store. httpx2 builds its default
context with truststore.SSLContext, which defers to the OS rather than to a bundled
certifi list. So a publicly-signed certificate (Let's Encrypt, DigiCert, …) works with no
configuration, and the fix for an internal CA is the ordinary Windows one — import it once
and every tool on the machine trusts it:
# Machine-wide (needs an elevated shell); use Cert:\CurrentUser\Root for just you.
Import-Certificate -FilePath C:\certs\your-ca.crt -CertStoreLocation Cert:\LocalMachine\RootIf you would rather scope it to this process only, SSL_CERT_FILE still overrides:
$env:SSL_CERT_FILE = "C:\certs\your-ca-chain.pem" # or SSL_CERT_DIR for a hashed dirTwo things that catch people out:
SSL_CERT_FILEreplaces the trust store rather than adding to it, so a process using it loses the Windows store — including every public CA. Importing intoCert:\LocalMachine\Rootavoids that problem entirely, which is why it is the first suggestion above.- Your server (or its reverse proxy) must present its full chain. A missing intermediate is the most common "the cert is valid but it still won't connect" cause, and the fix is on the server — the client only needs the root.
Project layout and extending
main.py entrypoint — connects the MCP servers, wires Chat + REPL
mcp_client.py MCP client (stdio / SSE / Streamable HTTP)
mcp_server.py the other direction — serve this agent to an MCP client
.mcp.json example Claude Code registration for mcp_server.py
smoke_test.py fast wiring checks — no API key, no network
.github/workflows/ci.yml runs ruff + smoke_test.py on push and PR
config.toml model + MCP server list (no secrets; committed)
pyproject.toml metadata, deps, and the ruff exemptions (lint config)
requirements.txt the same deps, for `pip install -r`
CLAUDE.md architecture + conventions, for AI coding agents
core/
chat.py agentic loop, tool routing, SYSTEM_PROMPT
claude.py Anthropic SDK wrapper
local_tools.py registry of every locally-executed tool
tools.py MCP <-> Anthropic bridge
claude_learned_schemas.py file editor, web_search, web_fetch
memory.py /memories store, persists across sessions
computer.py screenshots + mouse/keyboard
browser.py Playwright DOM surfing
documents.py LibreOffice / pandoc conversion
kernel.py persistent IPython kernel
processes.py ConPTY — commands that prompt
config_edit.py comment-preserving YAML/TOML/JSON edits
data.py DuckDB queries
files.py recoverable deletes
text_embeddings.py vector embeddings from a private HTTP server
output.py shared output trimming + image results
cli.py prompt_toolkit REPL
- Add an MCP server: add an entry under
[mcp].serversinconfig.toml— see "Configuration" above for both entry shapes (urlfor Streamable HTTP,commandfor a local stdio server main.py launches itself). Its tools appear to Claude automatically once it connects. A one-off Python stdio script can also be passed as an argument instead (python main.py path/to/server.py) without touching config.toml. - Add a local tool: write a module exposing
TOOLS,handles(name), andasync execute(name, tool_input), then add it toMODULESincore/local_tools.py. That's the only registration step. UpdateSYSTEM_PROMPTincore/chat.pytoo — it describes the tool set to Claude. - Keep the list lean. Tool-selection accuracy degrades past roughly 30–50 tools, so prefer one tool with a mode parameter over several near-duplicates, and don't wrap a command PowerShell could already run.
Check every configured server on its own with python mcp_client.py — it connects to each
in turn, lists its tools, and reports failures without starting the chat.
Inspecting an MCP server — mcp_client.py
mcp_client.py is the inspector for this project. It connects to your servers, lists what
they expose, and calls a tool — no install, no Node, no browser tab:
python mcp_client.py # every configured server: connect, list tools
python mcp_client.py -s unreal --schema # one server, with each tool's input schema
python mcp_client.py --prompts --resources # also list prompts and resources
python mcp_client.py -s n8n --call list_flows --args '{"limit": 5}'
python mcp_client.py --url http://host:8000/mcp --token-env N8N_MCP_TOKENIt builds each client through the same config.toml and the same build_client() the app
uses, which is the reason to prefer it over the generic
MCP Inspector. That one is a Node package
run through npx, and it asks you to retype each server's address and token into a browser —
so what it tests is not what the app is configured to do. This has already mattered here: an
earlier version of this script built its own clients and forced HTTP on every entry, which
made it report a stdio server as unreachable while python main.py was talking to it happily.
--token-env names the variable holding the bearer token rather than taking the token, so it
stays out of your shell history. A server that implements no prompts or resources says so
rather than looking broken.
Two things Node is still involved in, neither of which you install: Playwright bundles its
own runtime — playwright/driver/node.exe, about 92 MB, launched against
driver/package/cli.js — because its Python package is a client for a JavaScript driver. It
is vendored inside the pip package, is not on your PATH, and PLAYWRIGHT_NODEJS_PATH
overrides it. And a command = ["node", …] entry in config.toml runs whatever MCP server
you point it at; that one is your dependency, not this project's.
MIT — use it, fork it, ship it. No warranty; see the file for the full text.