@foldkit/vite-plugin 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,6 +44,18 @@ The plugin uses Vite's WebSocket connection to communicate between the dev serve
44
44
 
45
45
  Model is preserved across hot reloads but cleared on manual browser refreshes, giving you control over when to reset your app.
46
46
 
47
+ ## DevTools MCP relay
48
+
49
+ Pass `devToolsMcpPort` to enable the relay that exposes your running Foldkit app to AI agents via the [`@foldkit/devtools-mcp`](https://www.npmjs.com/package/@foldkit/devtools-mcp) MCP server:
50
+
51
+ ```typescript
52
+ plugins: [foldkit({ devToolsMcpPort: 9988 })]
53
+ ```
54
+
55
+ When set, the plugin opens a separate WebSocket server on the given port. The MCP server connects to it and forwards typed `Request` and `Response` frames between AI agents and your runtime. Without `devToolsMcpPort` (the default), the relay is not started and the plugin behaves exactly as before.
56
+
57
+ See the [DevTools MCP documentation](https://foldkit.dev/ai/mcp) for setup, the available tools, and how dispatch validation works.
58
+
47
59
  ## License
48
60
 
49
61
  MIT
package/dist/index.d.ts CHANGED
@@ -1,3 +1,13 @@
1
1
  import type { Plugin } from 'vite';
2
- export declare const foldkit: () => Plugin;
2
+ /** Options for the `foldkit` Vite plugin. */
3
+ export type FoldkitPluginOptions = Readonly<{
4
+ /**
5
+ * Port for the WebSocket server that exposes the DevTools relay to an
6
+ * external MCP server. When `undefined` (the default), no MCP relay is
7
+ * started. When set, the plugin listens on this port for connections from
8
+ * the Foldkit DevTools MCP server.
9
+ */
10
+ devToolsMcpPort?: number;
11
+ }>;
12
+ export declare const foldkit: (options?: FoldkitPluginOptions) => Plugin;
3
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAA;AAKjD,eAAO,MAAM,OAAO,QAAO,MAO1B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,MAAM,EAAkC,MAAM,MAAM,CAAA;AAGlE,6CAA6C;AAC7C,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB,CAAC,CAAA;AAqBF,eAAO,MAAM,OAAO,GAAI,UAAS,oBAAyB,KAAG,MAO5D,CAAA"}
package/dist/index.js CHANGED
@@ -1,14 +1,24 @@
1
+ import { Array, Effect, Either, HashMap, HashSet, Match, Option, Ref, Schema as S, pipe, } from 'effect';
2
+ import { EventFrame, RequestFrame, ResponseFrame, ResponseRuntimes, } from 'foldkit/devtools-protocol';
3
+ import { WebSocketServer } from 'ws';
1
4
  let preservedModel = undefined;
2
5
  let isHmrReload = false;
3
- export const foldkit = () => {
6
+ const connectedRuntimesRef = Ref.unsafeMake(HashMap.empty());
7
+ const mcpClientsRef = Ref.unsafeMake(HashSet.empty());
8
+ const clientConnectionsRef = Ref.unsafeMake(HashMap.empty());
9
+ const trackedClientsRef = Ref.unsafeMake(HashSet.empty());
10
+ let viteServer = null;
11
+ let mcpWebSocketServer = null;
12
+ export const foldkit = (options = {}) => {
4
13
  return {
5
14
  name: 'foldkit-hmr',
6
15
  apply: 'serve',
7
- configureServer,
16
+ configureServer: server => configureServer(server, options),
8
17
  handleHotUpdate,
9
18
  };
10
19
  };
11
- const configureServer = (server) => {
20
+ const configureServer = (server, options) => {
21
+ viteServer = server;
12
22
  server.ws.on('foldkit:preserve-model', model => {
13
23
  preservedModel = model;
14
24
  });
@@ -19,6 +29,140 @@ const configureServer = (server) => {
19
29
  }
20
30
  isHmrReload = false;
21
31
  });
32
+ server.ws.on('foldkit:devTools:event', (data, client) => {
33
+ ensureClientTracked(client);
34
+ handleBrowserEventFrame(data, client);
35
+ });
36
+ server.ws.on('foldkit:devTools:response', (data) => {
37
+ handleBrowserResponseFrame(data);
38
+ });
39
+ if (options.devToolsMcpPort !== undefined) {
40
+ startMcpWebSocketServer(options.devToolsMcpPort);
41
+ server.httpServer?.on('close', stopMcpWebSocketServer);
42
+ }
43
+ };
44
+ const startMcpWebSocketServer = (port) => {
45
+ const wss = new WebSocketServer({ port });
46
+ mcpWebSocketServer = wss;
47
+ wss.on('error', error => {
48
+ console.error(`[foldkit:devTools] MCP relay failed to start on port ${port}; continuing without the relay`, error);
49
+ mcpWebSocketServer = null;
50
+ });
51
+ wss.on('connection', client => {
52
+ Effect.runSync(Ref.update(mcpClientsRef, HashSet.add(client)));
53
+ const total = HashSet.size(Effect.runSync(Ref.get(mcpClientsRef)));
54
+ console.log(`[foldkit:devTools] MCP client connected (${total} total)`);
55
+ client.on('message', raw => {
56
+ handleMcpMessage(client, raw.toString());
57
+ });
58
+ client.on('close', () => {
59
+ Effect.runSync(Ref.update(mcpClientsRef, HashSet.remove(client)));
60
+ const remaining = HashSet.size(Effect.runSync(Ref.get(mcpClientsRef)));
61
+ console.log(`[foldkit:devTools] MCP client disconnected (${remaining} remaining)`);
62
+ });
63
+ client.on('error', error => {
64
+ console.error('[foldkit:devTools] MCP client error', error);
65
+ });
66
+ });
67
+ console.log(`[foldkit:devTools] MCP relay listening on ws://localhost:${port}`);
68
+ };
69
+ const stopMcpWebSocketServer = () => {
70
+ const clients = Effect.runSync(Ref.get(mcpClientsRef));
71
+ HashSet.forEach(clients, client => {
72
+ client.close();
73
+ });
74
+ Effect.runSync(Ref.set(mcpClientsRef, HashSet.empty()));
75
+ mcpWebSocketServer?.close();
76
+ mcpWebSocketServer = null;
77
+ console.log('[foldkit:devTools] MCP relay stopped');
78
+ };
79
+ const handleBrowserEventFrame = (data, client) => {
80
+ const decoded = S.decodeUnknownEither(EventFrame)(data);
81
+ Either.match(decoded, {
82
+ onLeft: error => {
83
+ console.warn('[foldkit:devTools] failed to decode browser event frame', error);
84
+ },
85
+ onRight: frame => Match.value(frame.event).pipe(Match.tagsExhaustive({
86
+ EventConnected: event => handleConnectedEvent(event, client),
87
+ EventDisconnected: handleDisconnectedEvent,
88
+ })),
89
+ });
90
+ };
91
+ const ensureClientTracked = (client) => {
92
+ const tracked = Effect.runSync(Ref.get(trackedClientsRef));
93
+ if (HashSet.has(tracked, client)) {
94
+ return;
95
+ }
96
+ Effect.runSync(Ref.update(trackedClientsRef, HashSet.add(client)));
97
+ client.socket.on('close', () => handleClientClose(client));
98
+ };
99
+ const handleClientClose = (client) => {
100
+ pipe(Effect.runSync(Ref.get(clientConnectionsRef)), HashMap.get(client), Option.match({
101
+ onNone: () => { },
102
+ onSome: connectionIds => HashSet.forEach(connectionIds, connectionId => {
103
+ Effect.runSync(Ref.update(connectedRuntimesRef, HashMap.remove(connectionId)));
104
+ console.log(`[foldkit:devTools] runtime pruned (socket close): ${connectionId}`);
105
+ }),
106
+ }));
107
+ Effect.runSync(Ref.update(clientConnectionsRef, HashMap.remove(client)));
108
+ Effect.runSync(Ref.update(trackedClientsRef, HashSet.remove(client)));
109
+ };
110
+ const handleBrowserResponseFrame = (data) => {
111
+ const decoded = S.decodeUnknownEither(ResponseFrame)(data);
112
+ Either.match(decoded, {
113
+ onLeft: error => {
114
+ console.warn('[foldkit:devTools] failed to decode browser response frame', error);
115
+ },
116
+ onRight: forwardResponseToMcpClients,
117
+ });
118
+ };
119
+ const handleConnectedEvent = (event, client) => {
120
+ Effect.runSync(Ref.update(connectedRuntimesRef, HashMap.set(event.runtime.connectionId, event.runtime)));
121
+ Effect.runSync(Ref.update(clientConnectionsRef, currentMap => {
122
+ const existing = pipe(HashMap.get(currentMap, client), Option.getOrElse(() => HashSet.empty()));
123
+ return HashMap.set(currentMap, client, HashSet.add(existing, event.runtime.connectionId));
124
+ }));
125
+ console.log(`[foldkit:devTools] runtime connected: ${event.runtime.connectionId} (${event.runtime.title})`);
126
+ };
127
+ const handleDisconnectedEvent = (event) => {
128
+ Effect.runSync(Ref.update(connectedRuntimesRef, HashMap.remove(event.connectionId)));
129
+ console.log(`[foldkit:devTools] runtime disconnected: ${event.connectionId}`);
130
+ };
131
+ const broadcastToMcpClients = (payload) => {
132
+ const clients = Effect.runSync(Ref.get(mcpClientsRef));
133
+ HashSet.forEach(clients, client => {
134
+ if (client.readyState === client.OPEN) {
135
+ client.send(payload);
136
+ }
137
+ });
138
+ };
139
+ const forwardResponseToMcpClients = (responseFrame) => {
140
+ broadcastToMcpClients(JSON.stringify(responseFrame));
141
+ };
142
+ const handleMcpMessage = (client, raw) => {
143
+ const decoded = S.decodeUnknownEither(S.parseJson(RequestFrame))(raw);
144
+ Either.match(decoded, {
145
+ onLeft: error => {
146
+ console.warn('[foldkit:devTools] failed to decode MCP request frame', error);
147
+ },
148
+ onRight: frame => Match.value(frame.request).pipe(Match.tag('RequestListRuntimes', () => replyListRuntimes(client, frame.id)), Match.orElse(() => forwardRequestToBrowsers(frame))),
149
+ });
150
+ };
151
+ const replyListRuntimes = (client, requestId) => {
152
+ const runtimes = pipe(Effect.runSync(Ref.get(connectedRuntimesRef)), HashMap.values, Array.fromIterable);
153
+ const responseFrame = {
154
+ id: requestId,
155
+ response: ResponseRuntimes({ runtimes }),
156
+ };
157
+ if (client.readyState === client.OPEN) {
158
+ client.send(JSON.stringify(responseFrame));
159
+ }
160
+ };
161
+ const forwardRequestToBrowsers = (frame) => {
162
+ if (viteServer === null) {
163
+ return;
164
+ }
165
+ viteServer.ws.send('foldkit:devTools:request', frame);
22
166
  };
23
167
  const handleHotUpdate = ({ server, modules, }) => {
24
168
  if (modules.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foldkit/vite-plugin",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Vite plugin for Foldkit hot module reloading with state preservation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -16,11 +16,19 @@
16
16
  "dist"
17
17
  ],
18
18
  "peerDependencies": {
19
+ "effect": "^3.18.2",
20
+ "foldkit": "^0.76.0",
19
21
  "vite": "^7.0.0"
20
22
  },
23
+ "dependencies": {
24
+ "ws": "^8.18.0"
25
+ },
21
26
  "devDependencies": {
22
- "typescript": "^5.9.3",
23
- "vite": "^7.3.1"
27
+ "@types/ws": "^8.5.13",
28
+ "effect": "^3.19.19",
29
+ "typescript": "^6.0.2",
30
+ "vite": "^7.3.1",
31
+ "foldkit": "0.76.0"
24
32
  },
25
33
  "keywords": [
26
34
  "vite",