@foldkit/devtools-mcp 0.24.1 → 0.25.0-canary.7ee214268c60
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 -12
- package/dist/relayClient.d.ts +37 -0
- package/dist/relayClient.d.ts.map +1 -0
- package/dist/relayClient.js +276 -0
- package/dist/relayLocation.d.ts +9 -1
- package/dist/relayLocation.d.ts.map +1 -1
- package/dist/relayLocation.js +57 -19
- package/dist/relayRegistry.d.ts +12 -2
- package/dist/relayRegistry.d.ts.map +1 -1
- package/dist/relayRegistry.js +74 -14
- package/dist/relayRegistryTrust.d.ts +19 -0
- package/dist/relayRegistryTrust.d.ts.map +1 -0
- package/dist/relayRegistryTrust.js +456 -0
- package/dist/server.js +6383 -1375
- package/dist/tools.d.ts +2 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +31 -50
- package/package.json +5 -5
- package/dist/webSocketClient.d.ts +0 -28
- package/dist/webSocketClient.d.ts.map +0 -1
- package/dist/webSocketClient.js +0 -134
package/dist/tools.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect } from 'effect';
|
|
2
|
-
import type
|
|
2
|
+
import { type RelayClient } from './relayClient.js';
|
|
3
3
|
type ToolResult = Readonly<{
|
|
4
4
|
content: ReadonlyArray<Readonly<{
|
|
5
5
|
type: 'text';
|
|
@@ -19,6 +19,6 @@ export type ToolDefinition = Readonly<{
|
|
|
19
19
|
* input via Effect Schema, dispatches a typed `Request` through the WebSocket
|
|
20
20
|
* relay, and formats the typed `Response` as MCP tool content.
|
|
21
21
|
*/
|
|
22
|
-
export declare const buildTools: (
|
|
22
|
+
export declare const buildTools: (relayClient: RelayClient) => ReadonlyArray<ToolDefinition>;
|
|
23
23
|
export {};
|
|
24
24
|
//# sourceMappingURL=tools.d.ts.map
|
package/dist/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAkB,MAAM,QAAQ,CAAA;AAOtD,OAAO,EAEL,KAAK,WAAW,EAEjB,MAAM,kBAAkB,CAAA;AA0LzB,KAAK,UAAU,GAAG,QAAQ,CAAC;IACzB,OAAO,EAAE,aAAa,CAAC,QAAQ,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAA;IAChE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAC,CAAA;AAEF,8HAA8H;AAC9H,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC;IACpC,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;CACzD,CAAC,CAAA;AAyEF;;;;GAIG;AACH,eAAO,MAAM,UAAU,gBACR,WAAW,KACvB,aAAa,CAAC,cAAc,CAsM9B,CAAA"}
|
package/dist/tools.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Array, Effect,
|
|
1
|
+
import { Array, Effect, Option, Schema } from 'effect';
|
|
2
2
|
import { MAX_DISPATCH_BATCH_SIZE, Request, } from 'foldkit/devtools-protocol';
|
|
3
|
-
|
|
3
|
+
import { errorReason, } from './relayClient.js';
|
|
4
|
+
const RUNTIME_ID_DESCRIPTION = 'Optional connection id of a specific Foldkit runtime. Defaults to the most recently connected runtime of the most recently started dev server. Call `foldkit_list_runtimes` to find runtimes in other applications, each with its `projectRoot`.';
|
|
4
5
|
const DEFAULT_LIST_MESSAGES_LIMIT = 50;
|
|
5
6
|
const RuntimeIdField = Schema.optional(Schema.String.annotate({ description: RUNTIME_ID_DESCRIPTION }));
|
|
6
7
|
const ListLimit = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 500 })).annotate({
|
|
@@ -129,40 +130,20 @@ const formatError = (reason) => ({
|
|
|
129
130
|
content: [{ type: 'text', text: `Error: ${reason}` }],
|
|
130
131
|
isError: true,
|
|
131
132
|
});
|
|
132
|
-
/**
|
|
133
|
-
* Read a display string from a caught error. Effect's `TimeoutError` carries no
|
|
134
|
-
* `message`, so `error.message` is `undefined` on a relay timeout; fall back to
|
|
135
|
-
* the error's string form (its tag) rather than surfacing `Error: undefined`.
|
|
136
|
-
*/
|
|
137
|
-
const errorReason = (error) => error.message ? error.message : String(error);
|
|
138
133
|
/**
|
|
139
134
|
* Decode a tool's raw input against its Effect Schema. Failure surfaces as an
|
|
140
135
|
* `Error` for the outer handler's `catchAll` to convert into a `ToolResult`.
|
|
141
136
|
*/
|
|
142
137
|
const decodeInput = (schema, rawInput) => Schema.decodeUnknownEffect(schema)(rawInput).pipe(Effect.mapError(error => new Error(`Invalid input: ${error.message}`)));
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
*/
|
|
148
|
-
const resolveRuntimeId = (wsClient, explicit) => {
|
|
149
|
-
if (explicit !== undefined) {
|
|
150
|
-
return Effect.succeed(explicit);
|
|
151
|
-
}
|
|
152
|
-
return Effect.gen(function* () {
|
|
153
|
-
const response = yield* wsClient.sendRequest(Request.RequestListRuntimes(), Option.none());
|
|
154
|
-
return yield* Match.value(response).pipe(Match.tag('ResponseRuntimes', ({ runtimes }) => Array.last(runtimes).pipe(Option.match({
|
|
155
|
-
onNone: () => Effect.fail(new Error('No connected Foldkit runtimes. Open a Foldkit dev page and try again.')),
|
|
156
|
-
onSome: runtime => Effect.succeed(runtime.connectionId),
|
|
157
|
-
}))), Match.tag('ResponseError', ({ reason }) => Effect.fail(new Error(reason))), Match.orElse(({ _tag }) => Effect.fail(new Error(`Unexpected response from RequestListRuntimes: ${_tag}`))));
|
|
158
|
-
});
|
|
159
|
-
};
|
|
138
|
+
const listedRuntimeOutput = ({ runtime, maybeProjectRoot }) => Option.match(maybeProjectRoot, {
|
|
139
|
+
onNone: () => runtime,
|
|
140
|
+
onSome: projectRoot => ({ ...runtime, projectRoot }),
|
|
141
|
+
});
|
|
160
142
|
const responseToToolResult = (response) => response._tag === 'ResponseError'
|
|
161
143
|
? formatError(response.reason)
|
|
162
144
|
: formatResult(response);
|
|
163
|
-
const callRuntimeRequest = (
|
|
164
|
-
const
|
|
165
|
-
const response = yield* wsClient.sendRequest(buildRequest(), Option.some(runtimeId));
|
|
145
|
+
const callRuntimeRequest = (relayClient, explicitRuntimeId, buildRequest) => Effect.gen(function* () {
|
|
146
|
+
const response = yield* relayClient.sendRequest(buildRequest(), Option.fromNullishOr(explicitRuntimeId));
|
|
166
147
|
return responseToToolResult(response);
|
|
167
148
|
}).pipe(Effect.catch(error => Effect.succeed(formatError(errorReason(error)))));
|
|
168
149
|
/**
|
|
@@ -170,16 +151,16 @@ const callRuntimeRequest = (wsClient, explicitRuntimeId, buildRequest) => Effect
|
|
|
170
151
|
* issues a typed `Request`, and formats the response. Used for every tool
|
|
171
152
|
* except `foldkit_list_runtimes`, which does not target a specific runtime.
|
|
172
153
|
*/
|
|
173
|
-
const runRuntimeTool = (inputSchema, buildRequest,
|
|
154
|
+
const runRuntimeTool = (inputSchema, buildRequest, relayClient) => (rawInput) => Effect.gen(function* () {
|
|
174
155
|
const input = yield* decodeInput(inputSchema, rawInput);
|
|
175
|
-
return yield* callRuntimeRequest(
|
|
156
|
+
return yield* callRuntimeRequest(relayClient, input.runtime_id, () => buildRequest(input));
|
|
176
157
|
}).pipe(Effect.catch(error => Effect.succeed(formatError(errorReason(error)))));
|
|
177
158
|
/**
|
|
178
159
|
* Build the read-only Foldkit DevTools tool definitions. Each tool decodes its
|
|
179
160
|
* input via Effect Schema, dispatches a typed `Request` through the WebSocket
|
|
180
161
|
* relay, and formats the typed `Response` as MCP tool content.
|
|
181
162
|
*/
|
|
182
|
-
export const buildTools = (
|
|
163
|
+
export const buildTools = (relayClient) => [
|
|
183
164
|
{
|
|
184
165
|
name: 'foldkit_get_model',
|
|
185
166
|
description: "Snapshot the current Model from a connected Foldkit runtime. By default the response is summarized (large arrays/records/strings collapse to `_summary` placeholders) to keep payloads small for AI agents. Pass `path` (e.g. 'root.session.user') to narrow to a subtree, and `expand: true` to receive the literal value at that path. Returns `{ value, atPath, summarized }`.",
|
|
@@ -187,7 +168,7 @@ export const buildTools = (wsClient) => [
|
|
|
187
168
|
handle: runRuntimeTool(GetModelInput, ({ path, expand }) => Request.RequestGetModel({
|
|
188
169
|
maybePath: Option.fromNullishOr(path),
|
|
189
170
|
expand: expand ?? false,
|
|
190
|
-
}),
|
|
171
|
+
}), relayClient),
|
|
191
172
|
},
|
|
192
173
|
{
|
|
193
174
|
name: 'foldkit_get_model_at',
|
|
@@ -197,7 +178,7 @@ export const buildTools = (wsClient) => [
|
|
|
197
178
|
index,
|
|
198
179
|
maybePath: Option.fromNullishOr(path),
|
|
199
180
|
expand: expand ?? false,
|
|
200
|
-
}),
|
|
181
|
+
}), relayClient),
|
|
201
182
|
},
|
|
202
183
|
{
|
|
203
184
|
name: 'foldkit_list_messages',
|
|
@@ -208,7 +189,7 @@ export const buildTools = (wsClient) => [
|
|
|
208
189
|
maybeSinceIndex: Option.fromNullishOr(since_index),
|
|
209
190
|
maybeChangedPathsMatch: Option.fromNullishOr(changed_paths_match),
|
|
210
191
|
fromEnd: from_end ?? false,
|
|
211
|
-
}),
|
|
192
|
+
}), relayClient),
|
|
212
193
|
},
|
|
213
194
|
{
|
|
214
195
|
name: 'foldkit_count_messages_by_tag',
|
|
@@ -217,7 +198,7 @@ export const buildTools = (wsClient) => [
|
|
|
217
198
|
handle: runRuntimeTool(CountMessagesByTagInput, ({ since_index, changed_paths_match }) => Request.RequestCountMessagesByTag({
|
|
218
199
|
maybeSinceIndex: Option.fromNullishOr(since_index),
|
|
219
200
|
maybeChangedPathsMatch: Option.fromNullishOr(changed_paths_match),
|
|
220
|
-
}),
|
|
201
|
+
}), relayClient),
|
|
221
202
|
},
|
|
222
203
|
{
|
|
223
204
|
name: 'foldkit_diff_models',
|
|
@@ -227,43 +208,43 @@ export const buildTools = (wsClient) => [
|
|
|
227
208
|
fromIndex: from_index,
|
|
228
209
|
toIndex: to_index,
|
|
229
210
|
maybeChangedPathsMatch: Option.fromNullishOr(changed_paths_match),
|
|
230
|
-
}),
|
|
211
|
+
}), relayClient),
|
|
231
212
|
},
|
|
232
213
|
{
|
|
233
214
|
name: 'foldkit_get_message',
|
|
234
215
|
description: 'Read a single Message history entry by absolute index. The response carries the SerializedEntry (tag, message body, commands, mountStarts, mountEnds, timestamp, `isModelChanged`, `changedPaths` for leaf-level mutations, `affectedPaths` adding their ancestor paths). Each entry in `commands` carries the Command name, `args` (`Some(record)` when the Command declared an args schema, `None` otherwise), and `maybeSubmodelPath` (`None` before the result resolves, `Some([])` for a resolved top-level Command, or `Some(tags)` for a resolved Submodel Command). `mountStarts` lists Mounts that fired during the render after this Message; `mountEnds` lists Mounts whose elements were unmounted during that render. Each Mount carries its `name` and `args` (`Some(record)` when the Mount declared an args schema, `None` otherwise). For Submodel-routed entries (tag matches `Got*Message`), the entry also carries `submodelPath` listing wrapper tags from outer to inner and `maybeLeafTag` naming the innermost child Message. Model snapshots are not included; call foldkit_get_model_at with `index - 1` (before) and `index` (after) to inspect Model state around the entry.',
|
|
235
216
|
inputSchema: toInputSchema(GetMessageInput),
|
|
236
|
-
handle: runRuntimeTool(GetMessageInput, ({ index }) => Request.RequestGetMessage({ index }),
|
|
217
|
+
handle: runRuntimeTool(GetMessageInput, ({ index }) => Request.RequestGetMessage({ index }), relayClient),
|
|
237
218
|
},
|
|
238
219
|
{
|
|
239
220
|
name: 'foldkit_get_init',
|
|
240
221
|
description: "Read the runtime's initial Model, the Commands returned from the application's `init` function, and the Mounts that fired during the first render. The init entry is the synthetic row at index -1 in the DevTools panel; this tool exposes the same data without time-travelling the runtime. `maybeModel` is `None` until the runtime has finished its first render and recorded init, then stays `Some` for the rest of the runtime's life. `commands` lists init-time Commands in the order they were produced, each with its name, `args` (`Some(record)` when the Command declared an args schema, `None` otherwise), and `maybeSubmodelPath` (`None` before the result resolves, `Some([])` for a resolved top-level Command, or `Some(tags)` for a resolved Submodel Command); `mountStarts` lists Mounts whose elements appeared in the initial render, each with its name and `args` (`Some(record)` when the Mount declared an args schema, `None` otherwise).",
|
|
241
222
|
inputSchema: toInputSchema(GetInitInput),
|
|
242
|
-
handle: runRuntimeTool(GetInitInput, () => Request.RequestGetInit(),
|
|
223
|
+
handle: runRuntimeTool(GetInitInput, () => Request.RequestGetInit(), relayClient),
|
|
243
224
|
},
|
|
244
225
|
{
|
|
245
226
|
name: 'foldkit_get_runtime_state',
|
|
246
227
|
description: "Snapshot the runtime's DevTools state: history bounds, current paused/live status, and whether init is recorded. Returns `currentIndex` (the absolute index of the most recent Message, or -1 when none), `startIndex` (the earliest absolute index still retained in the rolling buffer), `totalEntries` (count of retained entries), `isPaused`, `maybePausedAtIndex` (`Some(index)` when paused, `None` otherwise), and `hasInitModel`. Use it to reason about what `foldkit_list_messages` and `foldkit_get_message` will see, and to detect whether the runtime is currently paused at a replayed snapshot.",
|
|
247
228
|
inputSchema: toInputSchema(GetRuntimeStateInput),
|
|
248
|
-
handle: runRuntimeTool(GetRuntimeStateInput, () => Request.RequestGetRuntimeState(),
|
|
229
|
+
handle: runRuntimeTool(GetRuntimeStateInput, () => Request.RequestGetRuntimeState(), relayClient),
|
|
249
230
|
},
|
|
250
231
|
{
|
|
251
232
|
name: 'foldkit_list_keyframes',
|
|
252
233
|
description: 'List the available keyframes (replayable Model snapshots) from a Foldkit runtime.',
|
|
253
234
|
inputSchema: toInputSchema(ListKeyframesInput),
|
|
254
|
-
handle: runRuntimeTool(ListKeyframesInput, () => Request.RequestListKeyframes(),
|
|
235
|
+
handle: runRuntimeTool(ListKeyframesInput, () => Request.RequestListKeyframes(), relayClient),
|
|
255
236
|
},
|
|
256
237
|
{
|
|
257
238
|
name: 'foldkit_replay_to_keyframe',
|
|
258
239
|
description: 'Time-travel a Foldkit runtime back to a previous Model snapshot. Pass `keyframe_index: -1` for the initial Model, or a non-negative index for the state right after that history entry. The runtime is paused at the snapshot until foldkit_resume is called.',
|
|
259
240
|
inputSchema: toInputSchema(ReplayToKeyframeInput),
|
|
260
|
-
handle: runRuntimeTool(ReplayToKeyframeInput, ({ keyframe_index }) => Request.RequestReplayToKeyframe({ keyframeIndex: keyframe_index }),
|
|
241
|
+
handle: runRuntimeTool(ReplayToKeyframeInput, ({ keyframe_index }) => Request.RequestReplayToKeyframe({ keyframeIndex: keyframe_index }), relayClient),
|
|
261
242
|
},
|
|
262
243
|
{
|
|
263
244
|
name: 'foldkit_resume',
|
|
264
245
|
description: 'Resume normal execution of a Foldkit runtime that was paused by foldkit_replay_to_keyframe.',
|
|
265
246
|
inputSchema: toInputSchema(ResumeInput),
|
|
266
|
-
handle: runRuntimeTool(ResumeInput, () => Request.RequestResume(),
|
|
247
|
+
handle: runRuntimeTool(ResumeInput, () => Request.RequestResume(), relayClient),
|
|
267
248
|
},
|
|
268
249
|
{
|
|
269
250
|
name: 'foldkit_get_message_schema',
|
|
@@ -271,27 +252,27 @@ export const buildTools = (wsClient) => [
|
|
|
271
252
|
inputSchema: toInputSchema(GetMessageSchemaInput),
|
|
272
253
|
handle: runRuntimeTool(GetMessageSchemaInput, ({ variant_tag }) => Request.RequestGetMessageSchema({
|
|
273
254
|
maybeVariantTag: Option.fromNullishOr(variant_tag),
|
|
274
|
-
}),
|
|
255
|
+
}), relayClient),
|
|
275
256
|
},
|
|
276
257
|
{
|
|
277
258
|
name: 'foldkit_dispatch_message',
|
|
278
259
|
description: "Dispatch a Message into a Foldkit runtime, as if the application itself produced it. Requires the runtime to have configured DevToolsConfig.Message; without it, dispatch is rejected. Call `foldkit_get_message_schema` with no arguments to enumerate the variants, then with `variant_tag` to learn one variant's exact payload shape, before constructing the Message object. The runtime decodes the payload and returns a clean error if it doesn't match. To dispatch several Messages in order with one call, use foldkit_dispatch_messages.",
|
|
279
260
|
inputSchema: toInputSchema(DispatchMessageInput),
|
|
280
|
-
handle: runRuntimeTool(DispatchMessageInput, ({ message }) => Request.RequestDispatchMessage({ message }),
|
|
261
|
+
handle: runRuntimeTool(DispatchMessageInput, ({ message }) => Request.RequestDispatchMessage({ message }), relayClient),
|
|
281
262
|
},
|
|
282
263
|
{
|
|
283
264
|
name: 'foldkit_dispatch_messages',
|
|
284
265
|
description: `Dispatch an ordered batch of Messages into a Foldkit runtime in one call, first to last, as if the application produced them in quick succession. Prefer this over repeated foldkit_dispatch_message calls when staging state that takes several Messages: reproducing a bug, filling a form, or building a history fixture. Takes 1 to ${MAX_DISPATCH_BATCH_SIZE} Messages. Validation is all-or-nothing: the runtime decodes every payload against the Message Schema before dispatching any of them, and one invalid entry rejects the whole batch with an error naming its zero-based position, so there is never a partially applied prefix to clean up. On success the response carries acceptedAtIndices, the predicted history index for each Message in request order; the runtime may still record its own Messages between entries (for example a Command completing mid-burst), exactly as with rapid user input, and Messages excluded from history via excludeFromHistory are dispatched but never recorded, so entries after one land below their predicted index. Requires DevToolsConfig.Message, like foldkit_dispatch_message.`,
|
|
285
266
|
inputSchema: toInputSchema(DispatchMessagesInput),
|
|
286
|
-
handle: runRuntimeTool(DispatchMessagesInput, ({ messages }) => Request.RequestDispatchMessages({ messages }),
|
|
267
|
+
handle: runRuntimeTool(DispatchMessagesInput, ({ messages }) => Request.RequestDispatchMessages({ messages }), relayClient),
|
|
287
268
|
},
|
|
288
269
|
{
|
|
289
270
|
name: 'foldkit_list_runtimes',
|
|
290
|
-
description: 'List Foldkit runtimes (browser tabs)
|
|
271
|
+
description: 'List Foldkit runtimes (browser tabs) connected to every Foldkit dev server under the project root, or else to every dev server at the nearest root that encloses the project root, oldest dev server first. Each runtime carries `projectRoot`, the root of the dev server it belongs to, unless the MCP server uses a fixed relay address.',
|
|
291
272
|
inputSchema: NO_INPUT_SCHEMA,
|
|
292
|
-
handle: () => Effect.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
})
|
|
273
|
+
handle: () => relayClient.listRuntimes.pipe(Effect.map(listed => formatResult({
|
|
274
|
+
_tag: 'ResponseRuntimes',
|
|
275
|
+
runtimes: Array.map(listed, listedRuntimeOutput),
|
|
276
|
+
})), Effect.catch(error => Effect.succeed(formatError(errorReason(error))))),
|
|
296
277
|
},
|
|
297
278
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foldkit/devtools-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0-canary.7ee214268c60",
|
|
4
4
|
"description": "MCP server exposing Foldkit DevTools to AI agents (Claude Code, Cursor, etc.)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/server.js",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
19
|
"effect": "4.0.0",
|
|
20
|
-
"foldkit": "
|
|
20
|
+
"foldkit": "0.169.0-canary.7ee214268c60"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
@@ -25,16 +25,16 @@
|
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@effect/platform-node": "4.0.0",
|
|
28
|
+
"@foldkit/vite-plugin": "0.28.0-canary.7ee214268c60",
|
|
28
29
|
"@types/node": "26.4.0",
|
|
29
30
|
"@types/ws": "^8.18.1",
|
|
30
31
|
"effect": "4.0.0",
|
|
32
|
+
"foldkit": "0.169.0-canary.7ee214268c60",
|
|
31
33
|
"rimraf": "^6.1.3",
|
|
32
34
|
"rolldown": "^1.2.11",
|
|
33
35
|
"typescript": "^7.0.2",
|
|
34
36
|
"vite": "^8.3.1",
|
|
35
|
-
"vitest": "^4.1.11"
|
|
36
|
-
"@foldkit/vite-plugin": "0.26.1",
|
|
37
|
-
"foldkit": "0.166.0"
|
|
37
|
+
"vitest": "^4.1.11"
|
|
38
38
|
},
|
|
39
39
|
"files": [
|
|
40
40
|
"dist"
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { type Cause, Effect, Option } from 'effect';
|
|
2
|
-
import { type Request, type Response } from 'foldkit/devtools-protocol';
|
|
3
|
-
/**
|
|
4
|
-
* A WebSocket client to the Foldkit Vite plugin's DevTools relay.
|
|
5
|
-
*
|
|
6
|
-
* Sends typed `Request`s and resolves with the matching `Response`. The
|
|
7
|
-
* `sendRequest` Effect fails with `TimeoutException` when no response arrives
|
|
8
|
-
* within the request timeout window, or with `Error` when no relay is
|
|
9
|
-
* connected or the send throws. Either way, no pending entry leaks.
|
|
10
|
-
*
|
|
11
|
-
* The client manages its own connection lifecycle in a background fiber:
|
|
12
|
-
* the initial connect is retried with exponential backoff, and any later
|
|
13
|
-
* disconnect (e.g. when the user restarts the Vite dev server) reconnects
|
|
14
|
-
* via the same loop. The MCP server can stay live across dev-server
|
|
15
|
-
* restarts and even when no dev server has started yet — `sendRequest`
|
|
16
|
-
* returns a clear "not connected" error in that window, and tools should
|
|
17
|
-
* surface it to the agent so the user can start a dev server and retry.
|
|
18
|
-
*
|
|
19
|
-
* Pending response correlators live in a client-owned Ref, not on the
|
|
20
|
-
* socket, so they survive reconnects: in-flight requests time out and
|
|
21
|
-
* future requests succeed once the new socket is open.
|
|
22
|
-
*/
|
|
23
|
-
export type WebSocketClient = Readonly<{
|
|
24
|
-
sendRequest: (request: typeof Request.Type, maybeRuntimeId: Option.Option<string>) => Effect.Effect<typeof Response.Type, Cause.TimeoutError | Error>;
|
|
25
|
-
close: Effect.Effect<void>;
|
|
26
|
-
}>;
|
|
27
|
-
export declare const connectWebSocketClient: <Services>(resolveUrl: Effect.Effect<string, never, Services>) => Effect.Effect<WebSocketClient, never, Services>;
|
|
28
|
-
//# sourceMappingURL=webSocketClient.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"webSocketClient.d.ts","sourceRoot":"","sources":["../src/webSocketClient.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,KAAK,EAIV,MAAM,EAIN,MAAM,EAKP,MAAM,QAAQ,CAAA;AACf,OAAO,EACL,KAAK,OAAO,EAEZ,KAAK,QAAQ,EAEd,MAAM,2BAA2B,CAAA;AAgBlC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACrC,WAAW,EAAE,CACX,OAAO,EAAE,OAAO,OAAO,CAAC,IAAI,EAC5B,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAClC,MAAM,CAAC,MAAM,CAAC,OAAO,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC,CAAA;IACpE,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;CAC3B,CAAC,CAAA;AA2DF,eAAO,MAAM,sBAAsB,GAAI,QAAQ,cACjC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC,KACjD,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,KAAK,EAAE,QAAQ,CAoI7C,CAAA"}
|
package/dist/webSocketClient.js
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { Console, Deferred, Duration, Effect, Exit, Fiber, HashMap, Option, Ref, Schedule, Schema, pipe, } from 'effect';
|
|
2
|
-
import { RequestFrame, ResponseFrame, } from 'foldkit/devtools-protocol';
|
|
3
|
-
import { WebSocket } from 'ws';
|
|
4
|
-
const REQUEST_TIMEOUT = Duration.seconds(10);
|
|
5
|
-
const INITIAL_RECONNECT_DELAY = Duration.millis(500);
|
|
6
|
-
const MAX_RECONNECT_DELAY = Duration.seconds(30);
|
|
7
|
-
const encodeRequestFrameToJson = Schema.encodeUnknownSync(Schema.fromJsonString(RequestFrame));
|
|
8
|
-
const generateRequestId = () => `req-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
9
|
-
const reconnectSchedule = Schedule.exponential(INITIAL_RECONNECT_DELAY).pipe(Schedule.modifyDelay(({ duration }) => Effect.succeed(Duration.min(duration, MAX_RECONNECT_DELAY))));
|
|
10
|
-
const relayUrlForLog = (url) => {
|
|
11
|
-
const parsed = new URL(url);
|
|
12
|
-
parsed.search = '';
|
|
13
|
-
return parsed.toString();
|
|
14
|
-
};
|
|
15
|
-
const attemptOpen = (url) => Effect.callback(resume => {
|
|
16
|
-
const socket = new WebSocket(url);
|
|
17
|
-
let settled = false;
|
|
18
|
-
socket.once('open', () => {
|
|
19
|
-
if (settled)
|
|
20
|
-
return;
|
|
21
|
-
settled = true;
|
|
22
|
-
resume(Effect.succeed(socket));
|
|
23
|
-
});
|
|
24
|
-
socket.once('error', error => {
|
|
25
|
-
if (settled)
|
|
26
|
-
return;
|
|
27
|
-
settled = true;
|
|
28
|
-
resume(Effect.fail(error));
|
|
29
|
-
});
|
|
30
|
-
return Effect.sync(() => {
|
|
31
|
-
if (!settled) {
|
|
32
|
-
socket.removeAllListeners();
|
|
33
|
-
socket.close();
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
const waitForClose = (socket) => Effect.callback(resume => {
|
|
38
|
-
const isAlreadyClosing = socket.readyState === WebSocket.CLOSED ||
|
|
39
|
-
socket.readyState === WebSocket.CLOSING;
|
|
40
|
-
if (isAlreadyClosing) {
|
|
41
|
-
resume(Effect.void);
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
const handler = () => {
|
|
46
|
-
resume(Effect.void);
|
|
47
|
-
};
|
|
48
|
-
socket.once('close', handler);
|
|
49
|
-
return Effect.sync(() => {
|
|
50
|
-
socket.off('close', handler);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
export const connectWebSocketClient = (resolveUrl) => Effect.gen(function* () {
|
|
55
|
-
const pendingResponsesRef = yield* Ref.make(HashMap.empty());
|
|
56
|
-
const currentSocketRef = yield* Ref.make(Option.none());
|
|
57
|
-
const isManuallyClosedRef = yield* Ref.make(false);
|
|
58
|
-
const capturedContext = yield* Effect.context();
|
|
59
|
-
const attachMessageHandler = (socket) => {
|
|
60
|
-
socket.on('message', raw => {
|
|
61
|
-
Effect.runForkWith(capturedContext)(handleIncomingMessage(raw, pendingResponsesRef));
|
|
62
|
-
});
|
|
63
|
-
socket.on('error', error => {
|
|
64
|
-
console.error(`[foldkit-devtools-mcp] socket error: ${error.message}`);
|
|
65
|
-
});
|
|
66
|
-
};
|
|
67
|
-
const openWithBackoff = pipe(resolveUrl, Effect.flatMap(url => attemptOpen(url).pipe(Effect.map(socket => ({ socket, url })), Effect.tapError(error => Console.error(`[foldkit-devtools-mcp] connect attempt to ${relayUrlForLog(url)} failed: ${error.message}`)))), Effect.retry(reconnectSchedule), Effect.orDie);
|
|
68
|
-
const maintainConnection = Effect.gen(function* () {
|
|
69
|
-
const { socket, url } = yield* openWithBackoff;
|
|
70
|
-
yield* Console.error(`[foldkit-devtools-mcp] connected to ${relayUrlForLog(url)}`);
|
|
71
|
-
attachMessageHandler(socket);
|
|
72
|
-
yield* Ref.set(currentSocketRef, Option.some(socket));
|
|
73
|
-
yield* waitForClose(socket);
|
|
74
|
-
const isManual = yield* Ref.get(isManuallyClosedRef);
|
|
75
|
-
if (isManual) {
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
yield* Ref.set(currentSocketRef, Option.none());
|
|
79
|
-
yield* Console.error('[foldkit-devtools-mcp] connection lost, reconnecting');
|
|
80
|
-
yield* maintainConnection;
|
|
81
|
-
});
|
|
82
|
-
const connectionFiber = yield* Effect.forkDetach(maintainConnection);
|
|
83
|
-
const sendRequest = (request, maybeRuntimeId) => Effect.gen(function* () {
|
|
84
|
-
const maybeSocket = yield* Ref.get(currentSocketRef);
|
|
85
|
-
const socket = yield* Option.match(maybeSocket, {
|
|
86
|
-
onNone: () => Effect.fail(new Error('Not connected to a Foldkit dev server. Start your Foldkit Vite dev server and retry the tool call.')),
|
|
87
|
-
onSome: candidate => candidate.readyState === WebSocket.OPEN
|
|
88
|
-
? Effect.succeed(candidate)
|
|
89
|
-
: Effect.fail(new Error('Foldkit dev server connection is reconnecting. Retry the tool call in a moment.')),
|
|
90
|
-
});
|
|
91
|
-
const id = generateRequestId();
|
|
92
|
-
const deferred = yield* Deferred.make();
|
|
93
|
-
yield* Ref.update(pendingResponsesRef, HashMap.set(id, deferred));
|
|
94
|
-
const frame = {
|
|
95
|
-
id,
|
|
96
|
-
maybeConnectionId: maybeRuntimeId,
|
|
97
|
-
request,
|
|
98
|
-
};
|
|
99
|
-
yield* Effect.try({
|
|
100
|
-
try: () => socket.send(encodeRequestFrameToJson(frame)),
|
|
101
|
-
catch: error => error instanceof Error
|
|
102
|
-
? error
|
|
103
|
-
: new Error(`Failed to send request: ${String(error)}`),
|
|
104
|
-
}).pipe(Effect.tapError(() => Ref.update(pendingResponsesRef, HashMap.remove(id))));
|
|
105
|
-
return yield* Deferred.await(deferred).pipe(Effect.timeout(REQUEST_TIMEOUT), Effect.onError(() => Ref.update(pendingResponsesRef, HashMap.remove(id))));
|
|
106
|
-
});
|
|
107
|
-
const close = Effect.gen(function* () {
|
|
108
|
-
yield* Ref.set(isManuallyClosedRef, true);
|
|
109
|
-
const maybeSocket = yield* Ref.get(currentSocketRef);
|
|
110
|
-
yield* Option.match(maybeSocket, {
|
|
111
|
-
onNone: () => Effect.void,
|
|
112
|
-
onSome: socket => Effect.sync(() => socket.close()),
|
|
113
|
-
});
|
|
114
|
-
yield* Fiber.interrupt(connectionFiber);
|
|
115
|
-
});
|
|
116
|
-
return { sendRequest, close };
|
|
117
|
-
});
|
|
118
|
-
const handleIncomingMessage = (raw, pendingResponsesRef) => {
|
|
119
|
-
const decoded = Schema.decodeUnknownExit(Schema.fromJsonString(ResponseFrame))(raw.toString());
|
|
120
|
-
return Exit.match(decoded, {
|
|
121
|
-
onFailure: error => Effect.sync(() => console.error('[foldkit-devtools-mcp] failed to decode frame', error)),
|
|
122
|
-
onSuccess: responseFrame => Effect.gen(function* () {
|
|
123
|
-
const map = yield* Ref.get(pendingResponsesRef);
|
|
124
|
-
const maybeDeferred = HashMap.get(map, responseFrame.id);
|
|
125
|
-
yield* Option.match(maybeDeferred, {
|
|
126
|
-
onNone: () => Effect.void,
|
|
127
|
-
onSome: deferred => Effect.gen(function* () {
|
|
128
|
-
yield* Ref.update(pendingResponsesRef, HashMap.remove(responseFrame.id));
|
|
129
|
-
yield* Deferred.succeed(deferred, responseFrame.response);
|
|
130
|
-
}),
|
|
131
|
-
});
|
|
132
|
-
}),
|
|
133
|
-
});
|
|
134
|
-
};
|