MCEC 3.0 (the Model Context Environment Controller) gives an AI agent eyes, hands, and a safe
front door on Windows (Windows computer use, in the sense Claude, Codex, and similar agents use the
term). This file is the self-reinforcing guidance loop: the canonical guidance an
agent needs, plus the recipe to dogfood it (drive MCEC through its own MCP server) so the guidance
stays honest. Each time MCEC changes, re-run the dogfood and refine the guidance below and in
AgentServer.Instructions.
The connect-time guidance an MCP client shows the model is authored in
src/Agent/AgentInstructions.md. It is embedded into the exe at build
time and returned in the MCP initialize response (result.instructions) via AgentServer.Instructions,
which loads the embedded file and collapses each blank-line-separated paragraph to one line.
Work the loop observe → target → act → observe.
- Target a window by
window(title substring),process(name without.exe),className, orforeground:true. At least one is required; a call with no target fails by design.- Observe:
querydumps the UI Automation tree (controlType, name, automationId, bounds, state, value; bounded bymaxDepthandmaxNodes);capturereturns a PNG (renders composited WinUI/WPF surfaces correctly). Check results before trusting them: acapturewitherrorCategory: capture-blankis a black/empty frame (minimized/cloaked/occluded/locked session); restore or foreground the window and retry, don't trust the image; acapture-fallbackwarning means PrintWindow was refused and the picture may be wrong; aquerywithtruncated:true(atree-truncatedwarning) hit the node cap; raisemaxNodesor target a deeper window.warningsare non-fatal;errorCategorytells you how to recover. (Shape:docs/agent_control.mdandagent-tool-result.schema.json.)- Act: prefer
invoke(byname/automationId/classname;actioninvoke|toggle|setvalue| setfocus|expand|collapse|select) over coordinate clicks.invokefast-fails if the control isn't present (it does not wait), sofind/wait-forthe control first; aninvokethat returnsno-targetmeans it hasn't appeared yet;wait-forit rather than retrying blindly. Useselectfor TabItem/ListItem/RadioButton. To drag (resize a window by its sizing border, move one by its title bar, or drag a slider/handle; noinvokefor these), use thedragtool: give afromandto, each an element{ by, value }or an absolute screen pixel{ x, y }, plus optionalpathwaypoints; the whole press-move-release is dispatched atomically (prefer it over hand-rollingmouse:lbd/mouse:mt/mouse:lbu, which can interleave with other input). To click a pointinvokecan't reach (a custom-drawn cell, a canvas/map coordinate, a bare pixel), use theclicktool. Before firing an app's own keyboard shortcut at a specific surface (e.g. a MAUI GraphicsView),focusit first; keystrokes only reach the foreground window's focused control.send_commandis the raw escape hatch for any other MCEC command (keystrokes, a single mouse action, launch). Re-queryafter acting; bounds have moved.- Verify with another
query/capture.Compose creatively. Many tasks have no single dedicated tool; build them from primitives. Launch an app with the
launchtool (preferred). Useinvokeaction:select for tabs/list/radios. Drag/resize/move with thedragtool; switch a tab byinvokeselect orclick; record a window by passing itsquery'd bounds as therecordregion; wait on window state with thewindowstool + atimeoutand acondition(appears/disappears/foreground). A capable agent uses the full command set; reach for a rawsend_commandbefore concluding something can't be done.
There is exactly one copy: edit that file. It is the observe → target → act → observe playbook
(targeting; observation with query/capture/record/displays; invoke, drag, click, focus and
send_command; the result
envelope; creative composition of primitives; the on-screen overlay; and the security gates). Nothing here
to keep in sync.
Three independent, off-by-default gates; see docs/agent_control.md:
AgentCommandsEnabled (the observation opt-in, separate from actuation), per-command Enabled, and
McpServerEnabled (HTTP floor, localhost-bound). Every agent action is logged with an AGENT-AUDIT:
line. An agent that hits "agent commands are disabled" should tell the user, not retry.
Three safety features layer on top (see docs/safety-emergency-stop-and-provisioning.md):
- Emergency stop: a global "dead man's switch" hotkey (default
Ctrl+Alt+Shift+S) the operator can hit from any window to instantly halt a session: it latches the actuation gate (every tool call is refused withemergency-stoppeduntil the operator re-arms), aborts in-flight actuation, and releases held input. It reacts to physical input only (injected keys are ignored viaLLKHF_INJECTED), so the agent can never trip or defeat it. Do not weaken the injected-key filter or the latch. - Isolated session provisioning: agents must not enable/disable commands in the installed
instance (a crash leaks enabled gates). Instead,
provision-session(gated by the operator'sAllowSessionProvisioningopt-in, set from File ▸ Settings ▸ Agent) hands the agent a disposable directory with its own agent-ready config; teardown is deleting the directory, and MCEC reaps orphaned session dirs on launch. The installed config is never touched. That same Agent tab lists the provisioned instances, lets the operator delete any an agent leaves behind, and (with the opt-in ticked) has a Provision new… button that mints one and shows the handoff. So provisioning can bootstrap itself (#296): the installedmcec.exe --mcpserves the bootstrap only — justprovision-session/end-session, every other tool refused withbootstrap-only— so a fresh install's agent has a first hop, while the full observe/act surface is still never served from Program Files. Keep the bootstrap restriction (Program.ProvisioningBootstrapOnly) and the meta-tools' own gates intact. The operator's Provision new… handoff dialog shows a two-step handoff: MCP client setup plus a ready-made agent briefing prompt (ProvisionedInstanceDialog.BuildAgentPrompt); the prompt points at the connect-time instructions rather than duplicating the playbook, keep it that way. - Command-access consent (#307): a
command-disabledrefusal is recovered by therequest-command-accessmeta-tool, never by editing config files. It shows the OPERATOR a consent dialog (CommandAccessConsentDialog) with three choices: allow these commands, allow these plus any later requests, or deny (sticky per instance; an emergency stop dismissing the prompt is NOT a deny and records nothing). Grants are in-memory only: the loaded table'sEnabledflags, never persisted;CommandInvoker.Saveserializes consent-granted commands as disabled (AgentConsent.WasGrantedByConsent) so the Commands window cannot quietly write a grant into mcec.commands. The dialog is protected from the agent by three layers: the call holdsAgentRuntime.InputGatewhile the prompt is up; dispatch freezes everything except theToolDescriptor.ServedDuringConsentobservation tools withconsent-pending(default false, so a new tool stays frozen; the freeze runs BEFORE the meta-tool branches, soprovision-sessioncannot mint a second, unfrozen instance mid-prompt); and the dialog registers itself withWindowResolveras never-a-target. Do not weaken any of the three, do not persist grants, and do not replace the dialog with MCP elicitation (that routes consent through the agent's own client; the party being constrained). Grants land on the keyCommandInvoker.ResolveGateKeyresolves (the entry Enqueue actually gates on); route any new name-resolution through that one resolver. To spare the agent discovering the gated set onecommand-disabledat a time (#324), theinitializeresult carries acommandAccessmap (enabledTools/gatedTools/enabledRawCommands) derived fromToolCatalog.CommandAccessDefaults(theProvisionedByDefaultflags — single source of truth, so it never drifts as defaults change), andsession-statusstamps the LIVE table viaAgentToolExecutor.LiveCommandAccess; keep both derived, never a hand-typed list.enabledRawCommandsis an explicit list, not a canary boolean, so a partial raw grant (onlychars:) can't read as the whole rawsend_commandsurface being open (#340 CR).
This is the proposal's success metric ("an MCP client mounts MCEC and completes a multi-step GUI
task"), encoded as a real, opt-in test: MCEC drives MCEC end-to-end with genuine keyboard + mouse
input; no StartProcess launch crutch.
It lives in tests/MCEControl.xUnit/Integration/AgentDesktopE2ETests.cs.
Because it drives the real desktop (global keystrokes, mouse, launching apps), it is skipped
unless MCEC_DESKTOP_E2E=1; dotnet test/CI never touch your desktop. Run it deliberately on an
interactive session:
dotnet build src/MCEControl.csproj -c Debug
$env:MCEC_DESKTOP_E2E=1 ; dotnet test --filter Category=DesktopE2E
What it does, all via the mcec.exe --mcp server's send_command/query/capture tools:
- Launch via the Win+R Run dialog (keyboard):
send_command winr(Win+R) →chars:<path to mcec.exe>→enter. This starts a second MCEC purely through keyboard input. - Open Help ▸ About (mouse + keyboard):
querythe new window's UIA tree to get the Help menu's bounding rect →mouse:mt,<x>,<y>+mouse:lbcto mouse-click it →key_ato press A and open About. (Pixel coords from UIA are normalized to InputSimulator's 0..65535 space using the physical primary-display size.) - Verify:
capture {window:"About"}returns a PNG image block;query {window:"About"}returns the dialog's UIA tree (Window:About, the version text,License Agreement,Button:OK). - Graceful shutdown:
key_escdismisses About (its OK button is the CancelButton) →alt_fopens the File menu →key_xchooses E&xit, so the controlled instance runs its normalShutDown()(saves settings, closes) rather than being killed. The test asserts the window is gone.
The actuation commands it relies on are enabled in a temporary mcec.commands it writes next to the
exe (winr, chars:, enter, mouse:, key_a, key_esc, alt_f, key_x) and removed afterward.
- Give the controlled copy
ActAsServer=false; otherwise its socket-server bind raises a Windows Firewall prompt that steals foreground and eats the keystrokes. - A freshly launched copy must be the foreground window for menu keystrokes to land; injected Win+R is reliable, but let the Run dialog focus before typing the path.
- MCEC is a WinExe, so a console pipeline (
Get-Content | exe) won't reliably capture its stdout; spawn it with explicit inherited pipe handles (the test usesProcesswithRedirectStandardInput/Output).
SetStatussetNotifyIcon.Text(a 127-char cap) to the full informational version → crashed GUI startup on long prerelease version strings. Now capped..commandsloading callednew Version(informationalVersion)(throws onx.y.z-branch+sha) and poppedMessageBoxprompts that would hang the headless--mcpprocess. Now usesSystem.Version.TryParseand suppresses load-path dialogs whenAgentRuntime.Headless.
The worked examples (the mcec hero GIF, this WinPrint one, and prompt demos like Paint → smiley → email) are cataloged for humans and agents in
docs/examples.md, with a template for adding more.
MCEC driving installed WinPrint is a marquee dogfooding example, but the WinPrint hero GIF
(hero-gui-win.gif) is owned and produced in the winprint repo,
agent-driven via the generic scripts/Generate-HeroGif.ps1 controller
(no producer script lives here). Follow winprint's authoritative recipe
docs/hero-gif-win.md; it records a
clean, window-only, overlay-off hero. mcec-side conveniences that recipe still hand-configures (a
bootstrap -NoOverlay switch, default-enabling windows/right/down/enter/run) are tracked as
controller-bootstrap follow-ups.
- User docs vs contributor docs.
docs/is published to GitHub Pages (operator guides: install, configuration, agent control, safety, remote control, examples).dev/holds contributor and maintainer material only (CI, releasing, code signing, agent-server architecture notes, evidence bundles, doc-image regeneration recipes); it is not part of the site. Release instructions:dev/RELEASING.md(cut frommain, neverdevelop). - No em dashes (—) in prose, anywhere in this repo. Use parentheses or semicolons for an aside; for a
bullet-point heading (
* [Link](url) — description) use a plain hyphen (-) or a colon (:) instead. This applies to Markdown docs and code comments alike. - Agent-facing guidance is part of "Done": not optional, not "later." Any change to how an agent
observes/targets/acts (a new tool, arg, failure mode, warning/error category, or driving technique)
MUST update the connect-time playbook in
src/Agent/AgentInstructions.md(the single source of truth, embedded into the exe and served asAgentServer.Instructions) in the same change. Updating a per-toolTool(...)description is not a substitute (those describe args); the instructions teach recovery and strategy. Read the trigger by principle, not keyword: it fires on feature work, not just dogfooding. Treat it like updating tests. - Doc UI images are dogfood artifacts. Every PNG/GIF under
docs/that shows running MCEC (Settings tabs, the provision handoff, Commands window,hero.gif) must be regenerated with MCEC's owncapture/recordtools, followingdev/doc-images.mdanddev/hero-gif.md. Maintainer-only recipes (e.g.dev/paint-smiley-email.md) live underdev/and are not site assets. NoDrawToBitmapexport tests or hand-taken screenshots for committed assets. - Build is strict:
Nullable=enable,TreatWarningsAsErrors=true, and house analyzers MCEC0001 (one top-level type per file) / MCEC0002 (no nested types). New code must be warning-clean. - Agent subsystem lives in
src/Agent/+ the MCP server insrc/Services/(AgentServer.csis a thin static facade overMcpStdioTransport/McpHttpTransport/JsonRpcDispatcher/AgentToolExecutor); commands plug into the existingCommand/CommandInvokerpattern. Dev notes:dev/agent-server-architecture.md. - Tests:
dotnet test tests/MCEControl.xUnit/MCEControl.xUnit.csproj. Before opening a PR, run the full suite — not a--filteron only the tests you touched. Filtered runs miss regressions in other fixtures (e.g. aSessionProvisionerguard can breakEndSessionTokenTestswhileSessionProvisionerTestsstill pass). When you change provisioning or anything that affects session copies,grepforSessionProvisioner.BinariesDirand update every fake-install fixture together (useProvisionTestFixtures.SeedMinimalInstallintests/MCEControl.xUnit/Helpers/ProvisionTestFixtures.cs). Worker/lifecycle helpers extracted from a service (e.g. #317) belong in their own source files per MCEC0002.