@foldkit/devtools-mcp 0.22.0-canary.b415a3e22be5 → 0.22.0-canary.eb11872b6977
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 +14 -20
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +4 -6
- package/dist/relayLocation.d.ts +9 -0
- package/dist/relayLocation.d.ts.map +1 -0
- package/dist/relayLocation.js +33 -0
- package/dist/relayRegistry.d.ts +5 -0
- package/dist/relayRegistry.d.ts.map +1 -0
- package/dist/relayRegistry.js +70 -0
- package/dist/server.js +6639 -344
- package/dist/webSocketClient.d.ts +1 -9
- package/dist/webSocketClient.d.ts.map +1 -1
- package/dist/webSocketClient.js +9 -12
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -36,21 +36,7 @@ pnpm add -D @foldkit/devtools-mcp
|
|
|
36
36
|
yarn add -D @foldkit/devtools-mcp
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
In `vite.config.ts`, pass `devToolsMcpPort` to the Foldkit plugin so it opens the relay:
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
import { defineConfig } from 'vite'
|
|
45
|
-
|
|
46
|
-
import { foldkit } from '@foldkit/vite-plugin'
|
|
47
|
-
|
|
48
|
-
export default defineConfig({
|
|
49
|
-
plugins: [foldkit({ devToolsMcpPort: 9988 })],
|
|
50
|
-
})
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
In your `Runtime.makeApplication` call, pass your `Message` Schema. The Runtime decodes every dispatched payload against it, returning a clean error if the shape does not match before it reaches your update function:
|
|
39
|
+
Pass your `Message` Schema to `Runtime.makeApplication` to let the agent dispatch Messages. The Runtime rejects a payload that does not match the Schema before it reaches update:
|
|
54
40
|
|
|
55
41
|
```typescript
|
|
56
42
|
Runtime.makeApplication({
|
|
@@ -108,17 +94,25 @@ High-frequency flows (drag-paint, scroll, keystroke) can fill the history buffer
|
|
|
108
94
|
Three components cooperate:
|
|
109
95
|
|
|
110
96
|
- **Browser bridge** (in `foldkit`): runs alongside DevTools, subscribes to the DevTools store, and exchanges typed frames over Vite's HMR WebSocket.
|
|
111
|
-
- **Vite plugin relay** (in `@foldkit/vite-plugin`):
|
|
97
|
+
- **Vite plugin relay** (in `@foldkit/vite-plugin`): serves a WebSocket endpoint on the dev server, publishes its address for discovery, and forwards traffic between browsers and MCP clients.
|
|
112
98
|
- **MCP server** (this package): runs as a Node child process under your AI agent, connects to the plugin's relay over WebSocket, and exposes the typed tools over MCP's stdio transport.
|
|
113
99
|
|
|
114
100
|
Multiple browser tabs can be connected at once and each is addressable by its connection id. Tabs that close (gracefully or not) are pruned from the live Runtime list automatically.
|
|
115
101
|
|
|
116
102
|
## Configuration
|
|
117
103
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
|
121
|
-
|
|
|
104
|
+
The MCP server looks for a running dev server in its project directory. If several relays match, it uses the most recently started one. It discovers the relay again when the dev server restarts.
|
|
105
|
+
|
|
106
|
+
| Environment variable | What it changes |
|
|
107
|
+
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
108
|
+
| `FOLDKIT_PROJECT_ROOT` | Project to search. Defaults to the working directory. A relay for that directory or one inside it can match. |
|
|
109
|
+
| `FOLDKIT_DEVTOOLS_MCP_PORT` | Skips discovery and connects to this port. Set it to the `devToolsMcpPort` in your Vite config. Without this setting, the server tries `9988` if discovery finds nothing. |
|
|
110
|
+
| `FOLDKIT_DEVTOOLS_MCP_HOST` | Overrides the hostname of a discovered relay or configured port. |
|
|
111
|
+
| `FOLDKIT_DEVTOOLS_RELAY_DIRECTORY` | Registry location. Defaults to a directory under `XDG_RUNTIME_DIR` when set, or under the OS temporary directory. Set it in both processes if they use different sandboxes. |
|
|
112
|
+
|
|
113
|
+
A relay discovered through the registry requires the token in its published address. The plugin will not publish that token into a directory owned by another user or readable by other users. A configured `devToolsMcpPort` opens a separate socket on every interface without a token.
|
|
114
|
+
|
|
115
|
+
On Windows, directory ownership cannot be verified, so automatic discovery is unavailable. Use `devToolsMcpPort` in the Vite config and set `FOLDKIT_DEVTOOLS_MCP_PORT` to the same port.
|
|
122
116
|
|
|
123
117
|
## Notes
|
|
124
118
|
|
package/dist/install.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAoEA;;;;;GAKG;AACH,eAAO,MAAM,OAAO,QAAO,IAkB1B,CAAA"}
|
package/dist/install.js
CHANGED
|
@@ -36,15 +36,13 @@ const printNextSteps = (alreadyRegistered) => {
|
|
|
36
36
|
console.log('');
|
|
37
37
|
console.log('Next steps:');
|
|
38
38
|
console.log('');
|
|
39
|
-
console.log(' 1.
|
|
40
|
-
console.log('');
|
|
41
|
-
console.log(' plugins: [foldkit({ devToolsMcpPort: 9988 })]');
|
|
42
|
-
console.log('');
|
|
43
|
-
console.log(' 2. Pass your Message Schema to Runtime.makeApplication (enables dispatch):');
|
|
39
|
+
console.log(' 1. Pass your Message Schema to Runtime.makeApplication (enables dispatch):');
|
|
44
40
|
console.log('');
|
|
45
41
|
console.log(' devTools: { Message }');
|
|
46
42
|
console.log('');
|
|
47
|
-
console.log('
|
|
43
|
+
console.log(' 2. Start your dev server, then restart your AI agent (Claude Code, Cursor, etc.).');
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log(' The server finds your dev server on its own. See https://foldkit.dev/ai/mcp');
|
|
48
46
|
console.log('');
|
|
49
47
|
console.log('Tools will appear under the foldkit-devtools server, e.g. foldkit_get_model, foldkit_dispatch_message.');
|
|
50
48
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Effect, Option } from 'effect';
|
|
2
|
+
export type Settings = Readonly<{
|
|
3
|
+
maybeConfiguredPort: Option.Option<string>;
|
|
4
|
+
maybeConfiguredHost: Option.Option<string>;
|
|
5
|
+
projectRoot: string;
|
|
6
|
+
}>;
|
|
7
|
+
export declare const loadSettings: Effect.Effect<Settings>;
|
|
8
|
+
export declare const resolveRelayUrl: (settings: Settings) => Effect.Effect<string, never, never> | Effect.Effect<string, never, import("./relayRegistry.js").RelayRegistryServices>;
|
|
9
|
+
//# sourceMappingURL=relayLocation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relayLocation.d.ts","sourceRoot":"","sources":["../src/relayLocation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAO/C,MAAM,MAAM,QAAQ,GAAG,QAAQ,CAAC;IAC9B,mBAAmB,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAC1C,mBAAmB,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAC1C,WAAW,EAAE,MAAM,CAAA;CACpB,CAAC,CAAA;AAEF,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAe5B,CAAA;AAkBrB,eAAO,MAAM,eAAe,aAAc,QAAQ,2HAmBjD,CAAA"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Config, Effect, Option } from 'effect';
|
|
2
|
+
import { discoverRelay } from './relayRegistry.js';
|
|
3
|
+
const LEGACY_DEFAULT_PORT = 9988;
|
|
4
|
+
const DEFAULT_HOST = 'localhost';
|
|
5
|
+
export const loadSettings = Effect.gen(function* () {
|
|
6
|
+
const maybeConfiguredPort = yield* Config.option(Config.String('FOLDKIT_DEVTOOLS_MCP_PORT'));
|
|
7
|
+
const maybeConfiguredHost = yield* Config.option(Config.String('FOLDKIT_DEVTOOLS_MCP_HOST'));
|
|
8
|
+
const maybeProjectRoot = yield* Config.option(Config.String('FOLDKIT_PROJECT_ROOT'));
|
|
9
|
+
return {
|
|
10
|
+
maybeConfiguredPort,
|
|
11
|
+
maybeConfiguredHost,
|
|
12
|
+
projectRoot: Option.getOrElse(maybeProjectRoot, () => process.cwd()),
|
|
13
|
+
};
|
|
14
|
+
}).pipe(Effect.orDie);
|
|
15
|
+
const relayUrl = (host, port) => `ws://${host}:${port}`;
|
|
16
|
+
const withConfiguredHost = (maybeConfiguredHost, url) => Option.match(maybeConfiguredHost, {
|
|
17
|
+
onNone: () => url,
|
|
18
|
+
onSome: host => {
|
|
19
|
+
const parsed = new URL(url);
|
|
20
|
+
parsed.hostname = host;
|
|
21
|
+
return parsed.toString();
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
export const resolveRelayUrl = (settings) => {
|
|
25
|
+
const configuredHost = Option.getOrElse(settings.maybeConfiguredHost, () => DEFAULT_HOST);
|
|
26
|
+
return Option.match(settings.maybeConfiguredPort, {
|
|
27
|
+
onSome: port => Effect.succeed(relayUrl(configuredHost, port)),
|
|
28
|
+
onNone: () => discoverRelay(settings.projectRoot).pipe(Effect.map(maybeRecord => Option.match(maybeRecord, {
|
|
29
|
+
onSome: record => withConfiguredHost(settings.maybeConfiguredHost, record.url),
|
|
30
|
+
onNone: () => relayUrl(configuredHost, LEGACY_DEFAULT_PORT),
|
|
31
|
+
}))),
|
|
32
|
+
});
|
|
33
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Effect, FileSystem, Option, Path } from 'effect';
|
|
2
|
+
import { RelayRecord } from 'foldkit/devtools-protocol';
|
|
3
|
+
export type RelayRegistryServices = FileSystem.FileSystem | Path.Path;
|
|
4
|
+
export declare const discoverRelay: (projectRoot: string) => Effect.Effect<Option.Option<RelayRecord>, never, RelayRegistryServices>;
|
|
5
|
+
//# sourceMappingURL=relayRegistry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relayRegistry.d.ts","sourceRoot":"","sources":["../src/relayRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,MAAM,EACN,UAAU,EACV,MAAM,EAEN,IAAI,EAIL,MAAM,QAAQ,CAAA;AACf,OAAO,EAGL,WAAW,EACZ,MAAM,2BAA2B,CAAA;AAOlC,MAAM,MAAM,qBAAqB,GAAG,UAAU,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CAAA;AA4FrE,eAAO,MAAM,aAAa,gBACX,MAAM,KAClB,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,qBAAqB,CAoCrE,CAAA"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Array, Config, Effect, FileSystem, Option, Order, Path, Schema, String, pipe, } from 'effect';
|
|
2
|
+
import { RELAY_REGISTRY_DIRECTORY_NAME, RELAY_REGISTRY_DIRECTORY_VARIABLE, RelayRecord, } from 'foldkit/devtools-protocol';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
const RUNTIME_DIRECTORY_VARIABLE = 'XDG_RUNTIME_DIR';
|
|
5
|
+
const RECORD_FILE_EXTENSION = '.json';
|
|
6
|
+
const RETIRING_RECORD_SUFFIX = '.retiring';
|
|
7
|
+
const decodeRelayRecord = Schema.decodeUnknownOption(Schema.fromJsonString(RelayRecord));
|
|
8
|
+
const relayRegistryDirectory = Effect.gen(function* () {
|
|
9
|
+
const path = yield* Path.Path;
|
|
10
|
+
const maybeConfigured = yield* Config.option(Config.String(RELAY_REGISTRY_DIRECTORY_VARIABLE));
|
|
11
|
+
const maybeRuntimeDirectory = yield* Config.option(Config.String(RUNTIME_DIRECTORY_VARIABLE));
|
|
12
|
+
return Option.getOrElse(maybeConfigured, () => path.join(Option.getOrElse(maybeRuntimeDirectory, tmpdir), RELAY_REGISTRY_DIRECTORY_NAME));
|
|
13
|
+
}).pipe(Effect.orDie);
|
|
14
|
+
const isProcessAlive = (pid) => {
|
|
15
|
+
try {
|
|
16
|
+
process.kill(pid, 0);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
return error instanceof Error && 'code' in error && error.code === 'EPERM';
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const readRecordFile = (filePath) => Effect.gen(function* () {
|
|
24
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
25
|
+
const raw = yield* fileSystem.readFileString(filePath);
|
|
26
|
+
return decodeRelayRecord(raw);
|
|
27
|
+
}).pipe(Effect.orElseSucceed(() => Option.none()));
|
|
28
|
+
const retireStaleRecord = (filePath, record) => Effect.gen(function* () {
|
|
29
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
30
|
+
const retiringPath = `${filePath}.${encodeURIComponent(record.id)}${RETIRING_RECORD_SUFFIX}`;
|
|
31
|
+
const wasRecordMoved = yield* fileSystem
|
|
32
|
+
.rename(filePath, retiringPath)
|
|
33
|
+
.pipe(Effect.as(true), Effect.orElseSucceed(() => false));
|
|
34
|
+
if (!wasRecordMoved) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const maybeRetiringRecord = yield* readRecordFile(retiringPath);
|
|
38
|
+
const isOriginalRecord = Option.exists(maybeRetiringRecord, retiringRecord => retiringRecord.id === record.id);
|
|
39
|
+
if (!isOriginalRecord) {
|
|
40
|
+
yield* fileSystem.link(retiringPath, filePath).pipe(Effect.ignore);
|
|
41
|
+
}
|
|
42
|
+
yield* fileSystem.remove(retiringPath, { force: true }).pipe(Effect.ignore);
|
|
43
|
+
});
|
|
44
|
+
const readLiveRecordFile = (filePath) => Effect.gen(function* () {
|
|
45
|
+
const maybeRecord = yield* readRecordFile(filePath);
|
|
46
|
+
if (Option.isSome(maybeRecord) && !isProcessAlive(maybeRecord.value.pid)) {
|
|
47
|
+
yield* retireStaleRecord(filePath, maybeRecord.value);
|
|
48
|
+
return Option.none();
|
|
49
|
+
}
|
|
50
|
+
return maybeRecord;
|
|
51
|
+
});
|
|
52
|
+
const newestFirst = Order.mapInput(Order.flip(Order.Number), record => record.startedAt);
|
|
53
|
+
export const discoverRelay = (projectRoot) => Effect.gen(function* () {
|
|
54
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
55
|
+
const path = yield* Path.Path;
|
|
56
|
+
const directory = yield* relayRegistryDirectory;
|
|
57
|
+
const isWithinProjectRoot = (candidatePath) => {
|
|
58
|
+
const relativePath = path.relative(projectRoot, candidatePath);
|
|
59
|
+
return (relativePath === '' ||
|
|
60
|
+
(relativePath !== '..' &&
|
|
61
|
+
!relativePath.startsWith(`..${path.sep}`) &&
|
|
62
|
+
!path.isAbsolute(relativePath)));
|
|
63
|
+
};
|
|
64
|
+
const fileNames = yield* fileSystem
|
|
65
|
+
.readDirectory(directory)
|
|
66
|
+
.pipe(Effect.orElseSucceed(() => []));
|
|
67
|
+
const recordFileNames = Array.filter(fileNames, String.endsWith(RECORD_FILE_EXTENSION));
|
|
68
|
+
const maybeRecords = yield* Effect.forEach(recordFileNames, recordFileName => readLiveRecordFile(path.join(directory, recordFileName)));
|
|
69
|
+
return pipe(maybeRecords, Array.getSomes, Array.filter(record => isWithinProjectRoot(record.root)), Array.sort(newestFirst), Array.head);
|
|
70
|
+
});
|