You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Shared state is scoped to one dev server; a microfrontend workspace needs it workspace-wide
Shared state today is authoritative on the server and synced to every client connected to that server — client mutations round-trip through it, snapshots replay on reconnect. The guide lists "cross-client coordination" as a reason to reach for it.
The boundary is the process: run N dev servers and you get N independent stores, and a plugin in one dev server can neither read nor mutate the state of another.
flowchart LR
subgraph shell["dev server — shell"]
A["shared state"]
end
subgraph remoteA["dev server — remote A"]
B["shared state"]
end
subgraph remoteB["dev server — remote B"]
C["shared state"]
end
A <-->|"RPC sync"| dockA["dock"]
B <-->|"RPC sync"| dockB["dock"]
C <-->|"RPC sync"| dockC["dock"]
A x--x B
B x--x C
Loading
That boundary makes absolute sense in the context of a single app, but it is a problem in a microfrontend workspace. Ours holds around 80 apps, and a developer serves a handful at a time — a shell plus whichever remotes they're working on, each in its own Vite process, each independently startable.
A module-federation host with remotes, or a monorepo serving several apps plus Storybook, have the same shape.
Everything a developer wants to see and act on in that setup is cross-server: which peers are up, what the shell's import map resolves to right now, which remote served a given module, restart just that one remote.
None of it fits in a store owned by one of the participants. remote-client relocates the UI but still keeps one session per dev server, so it doesn't aggregate either.
What we ended up doing
We wanted a dock showing the whole workspace, and ended up building a small registry outside DevTools to get it: a Vite plugin every app inherits from a shared config, where the first process to start claims a fixed local port and hosts a registry the others join, with a survivor taking over if that process exits. It carries peer discovery, health, and start/stop.
flowchart LR
subgraph shell["dev server — shell"]
P1["registry plugin"]
end
subgraph remoteA["dev server — remote A"]
P2["registry plugin"]
end
subgraph remoteB["dev server — remote B"]
P3["registry plugin"]
end
H["registry — hosted by whichever process started first"]
P1 <--> H
P2 <--> H
P3 <--> H
H --> dock["one dock, whole workspace"]
H --> agent["one MCP endpoint, fixed port"]
Loading
It works, but it's a parallel devtools stack sitting next to the native vite tool chain — its own transport, its own registration, its own state — and it can't see anything DevTools already knows about the module graph.
A single entry point matters more for an agent than for a dock
For the browser UI, one context per dev server is mostly sufficient — you open the dock of whichever server you're looking at, and a human can hold "this dock is remote A" in their head.
An agent has no equivalent move: it doesn't open a dock, it holds a set of endpoints for a whole session.
A constellation of them costs it in three specific ways:
Context bloat: Every connected MCP server's tool and resource definitions sit in the model's context.
N dev servers running the same integrations means N copies of near-identical schemas, differing only by which server they belong to — paid for on every turn, and the agent still has to work out which copy to call.
Discovery and lifetime: An agent session outlives any individual dev server and usually discovers tools once, at connect time.
Endpoints that appear when a server starts and vanish when it stops leave the agent with a stale view: a remote started ten minutes into the session is invisible, one that has stopped is a dead entry it keeps reaching for.
A single endpoint that persists independently of the dev servers turns "which endpoints exist" from a moving target into a constant, with only the contents changing.
#455 looks like it already has the right shape here — a standalone vite-devtools mcp command is exactly an endpoint whose lifetime isn't tied to any one dev server. What it doesn't do is see the dev servers that are running.
Streaming common state: Another issue with multiple mcp agents, is data freshness on each consumer.
One workspace store makes "which peers are up" a single resource an agent can subscribe to.
N stores make it N subscriptions to stitch together, and no resource can describe the workspace at all.
The lifetime part in a picture:
sequenceDiagram
participant agent as agent session
participant shell as dev server — shell
participant remote as dev server — remote A
shell->>agent: endpoint exists
agent->>shell: connect, discover tools
Note over agent: endpoint set fixed here
remote->>remote: starts ten minutes later
Note over agent,remote: never discovered — not in the agent's config
shell--xagent: exits
Note over agent: dead entry, still retried
Loading
Suggested solution
Expand shared state so a single instance can span dev servers:
The sync boundary becomes the workspace rather than the process, with each contributing server identifiable in the store.
Because protocol adapters already build their resource surface from sharedState.keys(), a cross-server store would give a workspace-level agent surface without a second mechanism — the MCP half comes along for free.
flowchart LR
subgraph shell["dev server — shell"]
C1["devtools context"]
end
subgraph remoteA["dev server — remote A"]
C2["devtools context"]
end
subgraph remoteB["dev server — remote B"]
C3["devtools context"]
end
W["workspace shared state — one instance, contributors identifiable"]
C1 <-->|"RPC sync"| W
C2 <-->|"RPC sync"| W
C3 <-->|"RPC sync"| W
W <--> dock["any dock sees every server"]
W -->|"derived"| mcp["MCP resources"]
Loading
I'm not attached to a particular mechanism — you know the constraints better than I do. Mostly I'd like to know whether this is a direction you'd consider at all, and whether it would belong in devframe's hub or in the Vite integration, before anyone writes code.
Happy to prototype, or to share what we run today if it's useful.
I'd love to contribute more significantly on this if you feel it's appropriate for the direction of the tooling, but i need some guidance before investing time in it.
Worth saying that most of what we need is read aggregation — seeing peers and their resolved config from any one dock — plus writes scoped to the server that owns the key. So the hardest version of this, several servers concurrently mutating one shared key, may not need solving to make it useful.
Alternative
Keeping it in userland, which is what we do now. It works, but only by substituting for primitives that aren't there, and each substitute is where the fragility lives:
A hardcoded well-known port, because there's no discovery channel. It can collide with anything else on the machine, and two checkouts of the workspace open at once contend for it.
A lock file plus pid liveness checks, because there's no ownership primitive. A hard kill leaves a stale lock, and reaping it is a heuristic rather than something we know.
Heartbeats and re-election, because there's no lifecycle signal between dev servers. The window where the hosting process has died and no survivor has taken over yet is real, even if it's short.
None of that is specific to our domain — it's generic cross-process plumbing, and it's fragile precisely because it's built outside the thing that owns the lifecycle.
DevTools already sits in each dev server's lifecycle, and already has a connection descriptor and an auth model to build on. That's why this felt like it might belong closer to the framework rather than being rebuilt, slightly differently and slightly wrong, by every workspace
that hits it.
Additional context
I realise process-scoped state may well be deliberate: one context per build graph is a coherent model.
And widening it raises real questions — whose root/config wins, key collisions between contributors, what happens when the process others are syncing through exits, and auth across processes.
On #455: I don't think anything above conflicts with it — it's the standalone-endpoint half, and this is the question of what that endpoint can see.
Clear and concise description of the problem
Shared state is scoped to one dev server; a microfrontend workspace needs it workspace-wide
Shared state today is authoritative on the server and synced to every client connected to that server — client mutations round-trip through it, snapshots replay on reconnect. The guide lists "cross-client coordination" as a reason to reach for it.
The boundary is the process: run N dev servers and you get N independent stores, and a plugin in one dev server can neither read nor mutate the state of another.
flowchart LR subgraph shell["dev server — shell"] A["shared state"] end subgraph remoteA["dev server — remote A"] B["shared state"] end subgraph remoteB["dev server — remote B"] C["shared state"] end A <-->|"RPC sync"| dockA["dock"] B <-->|"RPC sync"| dockB["dock"] C <-->|"RPC sync"| dockC["dock"] A x--x B B x--x CThat boundary makes absolute sense in the context of a single app, but it is a problem in a microfrontend workspace. Ours holds around 80 apps, and a developer serves a handful at a time — a shell plus whichever remotes they're working on, each in its own Vite process, each independently startable.
A module-federation host with remotes, or a monorepo serving several apps plus Storybook, have the same shape.
Everything a developer wants to see and act on in that setup is cross-server: which peers are up, what the shell's import map resolves to right now, which remote served a given module, restart just that one remote.
None of it fits in a store owned by one of the participants.
remote-clientrelocates the UI but still keeps one session per dev server, so it doesn't aggregate either.What we ended up doing
We wanted a dock showing the whole workspace, and ended up building a small registry outside DevTools to get it: a Vite plugin every app inherits from a shared config, where the first process to start claims a fixed local port and hosts a registry the others join, with a survivor taking over if that process exits. It carries peer discovery, health, and start/stop.
flowchart LR subgraph shell["dev server — shell"] P1["registry plugin"] end subgraph remoteA["dev server — remote A"] P2["registry plugin"] end subgraph remoteB["dev server — remote B"] P3["registry plugin"] end H["registry — hosted by whichever process started first"] P1 <--> H P2 <--> H P3 <--> H H --> dock["one dock, whole workspace"] H --> agent["one MCP endpoint, fixed port"]It works, but it's a parallel devtools stack sitting next to the native vite tool chain — its own transport, its own registration, its own state — and it can't see anything DevTools already knows about the module graph.
A single entry point matters more for an agent than for a dock
For the browser UI, one context per dev server is mostly sufficient — you open the dock of whichever server you're looking at, and a human can hold "this dock is remote A" in their head.
An agent has no equivalent move: it doesn't open a dock, it holds a set of endpoints for a whole session.
A constellation of them costs it in three specific ways:
Context bloat: Every connected MCP server's tool and resource definitions sit in the model's context.
N dev servers running the same integrations means N copies of near-identical schemas, differing only by which server they belong to — paid for on every turn, and the agent still has to work out which copy to call.
Discovery and lifetime: An agent session outlives any individual dev server and usually discovers tools once, at connect time.
Endpoints that appear when a server starts and vanish when it stops leave the agent with a stale view: a remote started ten minutes into the session is invisible, one that has stopped is a dead entry it keeps reaching for.
A single endpoint that persists independently of the dev servers turns "which endpoints exist" from a moving target into a constant, with only the contents changing.
#455 looks like it already has the right shape here — a standalone
vite-devtools mcpcommand is exactly an endpoint whose lifetime isn't tied to any one dev server. What it doesn't do is see the dev servers that are running.Streaming common state: Another issue with multiple mcp agents, is data freshness on each consumer.
One workspace store makes "which peers are up" a single resource an agent can subscribe to.
N stores make it N subscriptions to stitch together, and no resource can describe the workspace at all.
The lifetime part in a picture:
Suggested solution
Expand shared state so a single instance can span dev servers:
The sync boundary becomes the workspace rather than the process, with each contributing server identifiable in the store.
Because protocol adapters already build their resource surface from
sharedState.keys(), a cross-server store would give a workspace-level agent surface without a second mechanism — the MCP half comes along for free.flowchart LR subgraph shell["dev server — shell"] C1["devtools context"] end subgraph remoteA["dev server — remote A"] C2["devtools context"] end subgraph remoteB["dev server — remote B"] C3["devtools context"] end W["workspace shared state — one instance, contributors identifiable"] C1 <-->|"RPC sync"| W C2 <-->|"RPC sync"| W C3 <-->|"RPC sync"| W W <--> dock["any dock sees every server"] W -->|"derived"| mcp["MCP resources"]I'm not attached to a particular mechanism — you know the constraints better than I do. Mostly I'd like to know whether this is a direction you'd consider at all, and whether it would belong in devframe's hub or in the Vite integration, before anyone writes code.
Happy to prototype, or to share what we run today if it's useful.
I'd love to contribute more significantly on this if you feel it's appropriate for the direction of the tooling, but i need some guidance before investing time in it.
Worth saying that most of what we need is read aggregation — seeing peers and their resolved config from any one dock — plus writes scoped to the server that owns the key. So the hardest version of this, several servers concurrently mutating one shared key, may not need solving to make it useful.
Alternative
Keeping it in userland, which is what we do now. It works, but only by substituting for primitives that aren't there, and each substitute is where the fragility lives:
None of that is specific to our domain — it's generic cross-process plumbing, and it's fragile precisely because it's built outside the thing that owns the lifecycle.
DevTools already sits in each dev server's lifecycle, and already has a connection descriptor and an auth model to build on. That's why this felt like it might belong closer to the framework rather than being rebuilt, slightly differently and slightly wrong, by every workspace
that hits it.
Additional context
I realise process-scoped state may well be deliberate: one context per build graph is a coherent model.
And widening it raises real questions — whose root/config wins, key collisions between contributors, what happens when the process others are syncing through exits, and auth across processes.
On #455: I don't think anything above conflicts with it — it's the standalone-endpoint half, and this is the question of what that endpoint can see.
Validations