dsh-mumuemulator-use
A DSH plugin that puts the local MuMu Player 12 emulator under model control.
It shells out to MuMu's own MuMuManager.exe and to adb — no Python, no MCP
server, no extra serialization layer. Windows only.
Seven tools, all prefixed mumu_:
| tool | what it does |
|---|---|
mumu_env |
show which install was found, from where, and where the cache lives |
mumu_list_instances |
every instance with index, name, Android version, run state, adb endpoint, pid |
mumu_instance |
launch / shutdown / restart / create / delete / rename / show_window / hide_window |
mumu_app |
list / info / install / uninstall / launch / close an app |
mumu_ui |
tap / swipe / long_press / text / key (framebuffer pixel coordinates) |
mumu_shell |
run a shell command inside the Android guest |
mumu_screenshot |
capture the screen; returns an image the model can look at |
Install
From npm
dsh plugin --profile <your-profile> add dsh-mumuemulator-use
That's it — dsh plugin add resolves the dependency, links it into the
profile, and appends dsh-mumuemulator-use to dsh.profile.bundles. Restart
the app or toggle the bundle to load it.
Desktop DSH users may see a pnpm warning
missing peer @deepseek-ai/dsh. This is expected and harmless: Desktop DSH ships as an Electron app and does not expose the host to the plugin'snode_modules. The plugin resolves the host through the Electron runtime, and functionality is unaffected.
From source
Only needed when developing the plugin against a checkout under
$PROFILE\plugins\. With $PROFILE = C:\Users\HP\.dsh\profiles\desktop:
# 1. put the files under $PROFILE\plugins\dsh-mumuemulator-use\
# 2. declare it as a dependency through the official channel
dsh plugin --profile desktop add file:plugins/dsh-mumuemulator-use
# the code imports @deepseek-ai/schemastery; a fresh profile sets
# autoInstallPeers: false, so make sure it is actually there
dsh plugin --profile desktop add @deepseek-ai/schemastery
# 3. link it as a junction (see "First: ..." below) — needed ONCE
cmd /c rmdir $PROFILE\node_modules\dsh-mumuemulator-use # link only — NEVER /s
New-Item -ItemType Junction -Path $PROFILE\node_modules\dsh-mumuemulator-use -Target $PROFILE\plugins\dsh-mumuemulator-use
# 4. reload the profile
Nothing else is needed: the plugin finds MuMu itself on first use.
Where the paths come from
mumu_env prints the whole trail. Resolution order:
- config override —
mumuRoot/mumuManagerPath/adbPathset on themumurow. - cache file,
<profile>\dsh-mumu-paths.json— reused when every recorded path still exists. - registry —
DisplayNamematching/MuMu/iinHKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*,HKLM\SOFTWARE\WOW6432Node\...\Uninstall\*,HKCU\...\Uninstall\*. For each hit three fields are tried in order:InstallLocation→DisplayIcon("C:\path\app.exe",0, quotes and,0stripped) →UninstallString(quotes stripped, directory taken). A candidate is accepted only if<root>\MuMuManager.exeor<root>\nx_main\MuMuManager.exeexists. - sweep — common install roots, as a last resort only.
adb prefers the newest nx_device\<version>\shell\adb.exe, then nx_main\adb.exe, then adb on PATH.
reg query writes in the OEM code page, so output is decoded as UTF-16LE (BOM), then strict UTF-8, then
strict GBK, then latin1 — this is why MuMu模拟器 does not come back as mojibake.
On this machine it resolves to:
MuMu root : D:\Program Files\Netease\MuMu
MuMuManager.exe : D:\Program Files\Netease\MuMu\nx_main\MuMuManager.exe
adb : D:\Program Files\Netease\MuMu\nx_device\15.0\shell\adb.exe
cache file : %PROFILE%\dsh-mumu-paths.json
version : {"version":"6.8.2.0"}
Configuration
Set it on the plugin's row in %PROFILE%\cordis.patch.yml:
- id: mumu # rowId is always "mumu"
config:
adbPath: 'D:\Program Files\Netease\MuMu\nx_device\15.0\shell\adb.exe'
screenshotIncludeImage: false
| key | default | meaning |
|---|---|---|
mumuRoot |
'' |
force the install root; skips detection |
mumuManagerPath |
'' |
force MuMuManager.exe; skips detection |
adbPath |
'' |
force adb.exe; empty = auto, falling back to adb on PATH |
cacheFile |
<profile>\dsh-mumu-paths.json |
where the resolved paths are cached |
commandTimeoutMs |
120000 |
per-command budget; the child is killed on expiry |
bootTimeoutMs |
300000 |
how long mumu_instance launch waits for Android |
adbConnectTimeoutMs |
60000 |
budget for adb connect |
screenshotIncludeImage |
true |
default for mumu_screenshot's includeImage argument |
Only mumuRoot/mumuManagerPath short-circuit detection; adbPath alone overrides just the adb side.
Known limitations
MuMuManager shcannot report failure. It exits 0 and prints nothing, whether the guest command succeeded or not, and MuMu refuses to run theadbsubcommand'sgetpropform.mumu_shelltherefore appendsecho "MUMU_SH_EXIT:$?"on its own line and parses the marker back out. A command line that the guest shell cannot parse (an unbalanced quote) swallows the marker and the exit code readsunavailable. A bare key name (go_home,go_back, …) is passed through untouched — those are MuMu shortcuts, not shell.control tool cmdis fire-and-forget.MuMuManager control -v 1 tool cmd -c "..."answers{"errcode":0,"errmsg":""}for every input, including nonsense, and never returns guest stdout. Usemumu_shellwhen you need output.show_window/hide_window/layout_window/tool func/tool cmddo not validate the instance index — they answererrcode 0for an index that does not exist. The plugin callsinfo -v <index>first (assertInstance) so those paths fail loudly instead of silently succeeding.- A stopped instance gives
errcode -201(player not running/vm not running, can not connect NemuShell !!) for touch, shell and app tools;mumu_screenshotreportsMuMu instance "1" is not running Android yet (player_state=stopped). adbmust be usable. With a wrongadbPathyou getspawn <path> ENOENT;mumu_shellstill works because it never touches adb.lib\index.jsprovenance. This file was reconstructed from the session's tool-call log after the original was deleted by mistake. It has been verified complete — line-for-line against everyreadsnapshot and byte-for-byte on the byte count — and all behaviour probes pass. SeeNOTES-dsh-mumuemulator-use.mdfor the accounting.- The framebuffer is landscape.
wm sizereports 900x1600 but screenshots come back 1600x900, andmumu_uicoordinates are framebuffer pixels, i.e. what you see in amumu_screenshotimage. - Screenshots are ~1.15 MB / 1600x900. Pass
includeImage: false(or setscreenshotIncludeImage: false) when polling, so the image does not enter the conversation on every call.
Changing the code
HMR is live in this profile, so editing a file under %PROFILE% reloads the plugin within ~1–2 s — no
restart. Measured with a behavioural probe: 1100 ms to swap one way, 2051 ms back.
First: node_modules\dsh-mumuemulator-use must be a junction
This is the load-bearing part of the whole setup, so check it before anything else.
1. Requirement. It must be a junction, not a real directory and not a hardlink:
New-Item -ItemType Junction -Path $PROFILE\node_modules\dsh-mumuemulator-use -Target $PROFILE\plugins\dsh-mumuemulator-use
2. How to check.
Get-Item $PROFILE\node_modules\dsh-mumuemulator-use | Select-Object LinkType, Target
# LinkType Target
# -------- ------
# Junction C:\Users\HP\.dsh\profiles\desktop\plugins\dsh-mumuemulator-use
3. How to repair. If LinkType comes back empty it is a real directory (a sync tool, a manual
Copy-Item, or a pnpm path did it). Delete it and re-create the junction as in step 1.
Never use a recursive delete here — not on the junction, and not on a directory that merely contains it. Both of these have already destroyed the source once:
Remove-Item $PROFILE\node_modules\dsh-mumuemulator-use -Recurse -Force— PowerShell follows the reparse point, so it deletes the contents of the target;plugins\dsh-mumuemulator-useis emptied too and you are left with two empty directories.cmd /c rmdir /s /q $PROFILE\node_modules— deleting the parent with/sdescends through the junction in exactly the same way./sis the whole problem; the junction is not.The only safe removal is a non-recursive
rmdiron the link itself:cmd /c rmdir "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-mumuemulator-use"Check
LinkTypefirst (Get-Item … | Select-Object LinkType); only aJunctionmay be deleted withrmdir. IfLinkTypeis empty it is a real directory andrmdirwill refuse it — copy the files out first, then delete. When apnpm/dsh plugininstall has to rebuildnode_modules, let it delete the junction by itself instead of clearing the directory out from under it.
4. Symptom when it is wrong: you edit the source, HMR reports nothing unusual, and the behaviour does
not change. The module URL becomes node_modules\dsh-mumuemulator-use\lib\index.js, so the watcher reloads the stale
copy while your edits sit in plugins\. No error, no log line — the reload simply succeeds against the
wrong file. This is quieter than the old hardlink breakage, which at least showed up as a size or File-ID
mismatch.
Then: why it reloads at all
Two settings make the reload happen:
%PROFILE%\cordis.patch.ymlenables the watcher:- id: hmr disabled: false config: root: ['.']- The junction keeps the module's real path at
%PROFILE%\plugins\dsh-mumuemulator-use\lib\index.js— outsidenode_modules, whichdsh-hmrignores by default.
Verify a reload landed by checking mumu_env's description in the tool list, or by calling a tool whose
behaviour you just changed.
If HMR is ever off, the fallback is a restart — editing files alone will not be picked up, because Node caches ES modules by URL for the lifetime of the process.
Saving half-written code
HMR fires on every save, and editors that truncate-then-write can briefly expose a partial file. This is not
worth guarding against for occasional post-development edits: a broken save just makes the tools error until
the next save repairs them, and restarting the profile clears it. The one thing to avoid is driving
mumu_* calls from a session while you are mid-save.
What a reload does to an in-flight call
A reload disposes the old generation, and the host-side MuMuManager.exe child of any running call
disappears ~1.5 s after the save. That does not break the call: it still returns at its natural
duration, still reports the real guest exit code, and leaves no zombie behind.
Measured on 2026-10-01 with mumu_shell {command: 'sleep 60'} and a background poller reporting
(Get-Process MuMuManager -EA SilentlyContinue).Count:
| scenario | host MuMuManager count |
wrapped call |
|---|---|---|
| control, no reload | 1 for the whole 20 s call |
returned at 20 579 ms |
| HMR reload at +6 s | 1 at +6 s → 0 at +7.5 s → 0 thereafter |
returned at 60 588 ms, guest exit code 0 |
external Stop-Process -Name MuMuManager at +6 s |
1 at +6 s → 0 at +7 s → 0 thereafter |
returned at 25 529 ms, guest exit code 0 |
So a reload does not disturb in-flight work and does not leak. To reproduce: start a long mumu_shell,
poll the count from a background pwsh job, and trigger the reload by saving a file under %PROFILE%.
Uninstall
From npm
dsh plugin --profile <your-profile> remove dsh-mumuemulator-use
This drops the dependency, the dsh.profile.bundles entry, and the linked
package. If you also applied the profile-patch edits (tool-plugin-manager /
hmr / tool-cordis), see "Profile patch edits" below.
From source
With $PROFILE = C:\Users\HP\.dsh\profiles\desktop:
# 1. remove the bundle through the official channel
dsh plugin --profile desktop remove dsh-mumuemulator-use
# 2. delete the junction — NON-RECURSIVE only (see "First: ..." above)
cmd /c rmdir "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-mumuemulator-use"
# 3. delete the source directory — this one is a real directory, so rmdir /s is safe here
cmd /c rmdir /s /q "$env:USERPROFILE\.dsh\profiles\desktop\plugins\dsh-mumuemulator-use"
# 4. if step 1 did not already do it, drop "dsh-mumuemulator-use" from
# dsh.profile.bundles in $PROFILE\package.json
# 5. delete the path cache
Remove-Item $PROFILE\dsh-mumu-paths.json
Profile patch edits
If you applied them, three rows were added to $PROFILE\cordis.patch.yml:
tool-plugin-manager (enabled), hmr, and tool-cordis.
Do not restore cordis.patch.yml.bak-before-mumu unless you want to undo
all three at once. That backup predates the whole round, so restoring it
also disables HMR and tool-plugin-manager. If you only want the mumu row
gone, delete the - id: mumu / - id: mumuemulator-use entry and leave the
rest alone.
No comments yet. Be the first to write one.