@foldkit/devtools-mcp 0.24.1 → 0.25.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/dist/tools.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Effect } from 'effect';
2
- import type { WebSocketClient } from './webSocketClient.js';
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: (wsClient: WebSocketClient) => ReadonlyArray<ToolDefinition>;
22
+ export declare const buildTools: (relayClient: RelayClient) => ReadonlyArray<ToolDefinition>;
23
23
  export {};
24
24
  //# sourceMappingURL=tools.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAyB,MAAM,QAAQ,CAAA;AAO7D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AA0L3D,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;AAsHF;;;;GAIG;AACH,eAAO,MAAM,UAAU,aACX,eAAe,KACxB,aAAa,CAAC,cAAc,CAsM9B,CAAA"}
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, Match, Option, Schema } from 'effect';
1
+ import { Array, Effect, Option, Schema } from 'effect';
2
2
  import { MAX_DISPATCH_BATCH_SIZE, Request, } from 'foldkit/devtools-protocol';
3
- const RUNTIME_ID_DESCRIPTION = 'Optional connection id of a specific Foldkit runtime. Defaults to the most recently connected runtime.';
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
- * Resolve a runtime id, defaulting to the most recently connected runtime when
145
- * the caller did not specify one. Failures (no runtimes connected, relay
146
- * error) surface as `Error` for the outer handler's `catchAll` to convert.
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 = (wsClient, explicitRuntimeId, buildRequest) => Effect.gen(function* () {
164
- const runtimeId = yield* resolveRuntimeId(wsClient, explicitRuntimeId);
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, wsClient) => (rawInput) => Effect.gen(function* () {
154
+ const runRuntimeTool = (inputSchema, buildRequest, relayClient) => (rawInput) => Effect.gen(function* () {
174
155
  const input = yield* decodeInput(inputSchema, rawInput);
175
- return yield* callRuntimeRequest(wsClient, input.runtime_id, () => buildRequest(input));
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 = (wsClient) => [
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
- }), wsClient),
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
- }), wsClient),
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
- }), wsClient),
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
- }), wsClient),
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
- }), wsClient),
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 }), wsClient),
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(), wsClient),
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(), wsClient),
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(), wsClient),
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 }), wsClient),
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(), wsClient),
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
- }), wsClient),
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 }), wsClient),
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 }), wsClient),
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) currently connected to the dev server.',
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.gen(function* () {
293
- const response = yield* wsClient.sendRequest(Request.RequestListRuntimes(), Option.none());
294
- return responseToToolResult(response);
295
- }).pipe(Effect.catch(error => Effect.succeed(formatError(errorReason(error))))),
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.24.1",
3
+ "version": "0.25.0",
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",
@@ -25,16 +25,16 @@
25
25
  },
26
26
  "devDependencies": {
27
27
  "@effect/platform-node": "4.0.0",
28
+ "@foldkit/vite-plugin": "0.28.0",
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",
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"}
@@ -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
- };