Skip to content

Architecture

Steven Blom edited this page Sep 14, 2026 · 1 revision

Architecture

RoseMCP is several processes, and which of them exist depends on whether a tray is running. This page shows both arrangements, then follows one live-app request from the agent to the running app and back.

The pieces

Process What it does Started by
RoseMcp.Server The executable an MCP client registers. Over stdio it either relays to a tray or runs a broker of its own; with --transport http it is a long-lived broker. Your MCP client, or you
RoseMcp.Tray The same broker, hosted in-process behind http://127.0.0.1:5077, with a window that shows every workspace and debug session. You, or Windows at sign-in
RoseMcp.Worker Owns one MSBuildWorkspace for one solution. All Roslyn work happens here. The broker, one per solution
RoseMcp.LiveApp The live-app host: one ICorDebug session and one XAML diagnostics session for one debugged process, built per architecture (x64, x86, arm64) to match the target. The broker, one per debug session
XAML tap RoseMcp.Xaml.Uwp.Tap.dll or RoseMcp.Xaml.WinUi.Tap.dll, a native provider loaded into the target app. Walks the visual tree, applies live edits, and draws the Rose panel. Injected by the live-app host
RoseMcp.Inspector A window for one debugged process. Owns nothing; reads and drives a session over the broker's operator API. The tray's Inspect, or you

Every child talks MCP over stdio to the process that started it, and every one exits when that stdin closes. Nothing is orphaned when a broker goes away.

Tray mode

With a tray running, every stdio session on the machine relays to it. The tray holds the workers and the debug sessions, so they are shared by every agent and visible in one window.

flowchart LR
    subgraph clients["Agents"]
        a1(["MCP client<br/>repo A"])
        a2(["MCP client<br/>repo B"])
    end

    s1["RoseMcp.Server<br/>stdio · TrayRelay"]
    s2["RoseMcp.Server<br/>stdio · TrayRelay"]

    subgraph tray["RoseMcp.Tray · http://127.0.0.1:5077"]
        broker["Broker<br/>WorkspaceManager<br/>LiveAppSessionManager"]
        window["Tray window"]
        operator["/operator API<br/>bearer token, minted per run"]
    end

    wa["RoseMcp.Worker<br/>A.sln"]
    wb["RoseMcp.Worker<br/>B.slnx"]
    host["RoseMcp.LiveApp<br/>win-x64"]
    inspector["RoseMcp.Inspector"]

    subgraph app["Debugged app"]
        target["App code"]
        tap["XAML tap<br/>+ Rose panel"]
    end

    a1 -- stdio --> s1
    a2 -- stdio --> s2
    s1 -- "MCP over http<br/>+ origin directory" --> broker
    s2 -- "MCP over http<br/>+ origin directory" --> broker
    window -. reads in-process .-> broker
    broker -- "MCP over stdio" --> wa
    broker -- "MCP over stdio" --> wb
    broker -- "MCP over stdio" --> host
    inspector -- "http + token" --> operator
    operator --> broker
    host -- ICorDebug --> target
    host -- "injects, then<br/>named pipe" --> tap
Loading
  • The relay adds one fact. A stdio process knows the directory its client started in, and an http broker serving every repository does not. TrayRelay sends that directory along with each call and changes nothing else, so a bare call still resolves to the right solution.
  • The tray launches the inspector. It hands over the port and its per-run token, so you never see the token unless you ask for it with Copy inspector command.
  • One warm worker per solution, whichever agent asked first.

Self-hosted mode

With no tray, a stdio server does everything itself. Its workers and debug sessions belong to that one client and end with it.

flowchart LR
    client(["MCP client"])

    subgraph server["RoseMcp.Server · stdio"]
        broker["Broker"]
    end

    worker["RoseMcp.Worker<br/>A.sln"]
    host["RoseMcp.LiveApp<br/>win-x64"]

    subgraph app["Debugged app"]
        target["App code"]
        tap["XAML tap<br/>+ Rose panel"]
    end

    client -- stdio --> broker
    broker -- "MCP over stdio" --> worker
    broker -- "MCP over stdio" --> host
    host -- ICorDebug --> target
    host -- "injects, then<br/>named pipe" --> tap
Loading

There is no listener in this arrangement, so there is no inspector. The Rose panel still works: it lives in the app, not in a window of ours.

RoseMcp.Server --transport http is the third option. It is the tray without the window: a long-lived broker with the same MCP endpoint and operator API, gated by ROSEMCP_TOKEN when that is set. If the token is minted instead, it is written to the server's log once.

Following a live-app request

An agent asks for the visual tree of an app it launched under the debugger:

sequenceDiagram
    autonumber
    participant Agent as MCP client
    participant Broker
    participant Host as RoseMcp.LiveApp
    participant Tap as XAML tap (in the app)

    Agent->>Broker: rose_xaml_tree(sessionId)
    Broker->>Host: forwards over stdio
    alt no provider resident yet
        Host->>Host: create named pipe rosemcp-xaml-{pid}-{guid}
        Host->>Tap: InitializeXamlDiagnosticsEx loads the tap
        Tap->>Tap: walk the tree, put the Rose panel up
        Tap->>Host: connect to the pipe
    end
    Host->>Tap: request frame
    Tap->>Tap: work on the app's UI thread
    Tap->>Host: reply frame
    Host->>Broker: LiveXamlTree
    Broker->>Agent: result, attributed to the session
Loading
  • Injection happens once per session. Every later request, including the inspector's, is a length-prefixed frame on the same pipe. The tap stays loaded until the app exits.
  • One request at a time. The tap does its work on the app's UI thread, so requests from the agent and from the inspector queue behind each other rather than interleaving.
  • A stopped app cannot answer XAML requests. While the debugger holds the target at a breakpoint, the UI thread that would serve the request is frozen too. Both the tool and the inspector report that rather than waiting.
  • The panel and the agent share a selection. An element picked by clicking in the app is what rose_xaml_selection returns, and an element the agent selects is outlined in the app.

Clone this wiki locally