@gtkx/cli 1.0.0-rc.1 → 1.0.0-rc.3

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.
Files changed (286) hide show
  1. package/dist/builder.d.ts +3 -2
  2. package/dist/builder.d.ts.map +1 -1
  3. package/dist/builder.js +2 -1
  4. package/dist/builder.js.map +1 -1
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +21 -6
  8. package/dist/cli.js.map +1 -1
  9. package/dist/codegen/freshness.d.ts +5 -5
  10. package/dist/codegen/freshness.d.ts.map +1 -1
  11. package/dist/codegen/freshness.js +26 -23
  12. package/dist/codegen/freshness.js.map +1 -1
  13. package/dist/codegen/report.d.ts +2 -1
  14. package/dist/codegen/report.d.ts.map +1 -1
  15. package/dist/codegen/report.js +4 -2
  16. package/dist/codegen/report.js.map +1 -1
  17. package/dist/codegen/run-codegen.d.ts +10 -10
  18. package/dist/codegen/run-codegen.d.ts.map +1 -1
  19. package/dist/codegen/run-codegen.js +72 -47
  20. package/dist/codegen/run-codegen.js.map +1 -1
  21. package/dist/codegen/store-resolver.d.ts +9 -6
  22. package/dist/codegen/store-resolver.d.ts.map +1 -1
  23. package/dist/codegen/store-resolver.js +24 -7
  24. package/dist/codegen/store-resolver.js.map +1 -1
  25. package/dist/commands/build.d.ts +4 -3
  26. package/dist/commands/build.d.ts.map +1 -1
  27. package/dist/commands/build.js +2 -1
  28. package/dist/commands/build.js.map +1 -1
  29. package/dist/commands/codegen.d.ts +2 -1
  30. package/dist/commands/codegen.d.ts.map +1 -1
  31. package/dist/commands/codegen.js +6 -4
  32. package/dist/commands/codegen.js.map +1 -1
  33. package/dist/commands/dev.d.ts +2 -1
  34. package/dist/commands/dev.d.ts.map +1 -1
  35. package/dist/commands/dev.js +2 -1
  36. package/dist/commands/dev.js.map +1 -1
  37. package/dist/commands/docs.d.ts +2 -1
  38. package/dist/commands/docs.d.ts.map +1 -1
  39. package/dist/commands/docs.js +25 -7
  40. package/dist/commands/docs.js.map +1 -1
  41. package/dist/dev/icon-dir.d.ts +2 -1
  42. package/dist/dev/icon-dir.d.ts.map +1 -1
  43. package/dist/dev/icon-dir.js +6 -3
  44. package/dist/dev/icon-dir.js.map +1 -1
  45. package/dist/dev/refresh-tracker.d.ts +5 -4
  46. package/dist/dev/refresh-tracker.d.ts.map +1 -1
  47. package/dist/dev/refresh-tracker.js +6 -5
  48. package/dist/dev/refresh-tracker.js.map +1 -1
  49. package/dist/dev/runner-deps.d.ts +2 -1
  50. package/dist/dev/runner-deps.d.ts.map +1 -1
  51. package/dist/dev/runner-deps.js +20 -4
  52. package/dist/dev/runner-deps.js.map +1 -1
  53. package/dist/dev/runner-main.d.ts +2 -1
  54. package/dist/dev/runner-main.d.ts.map +1 -1
  55. package/dist/dev/runner-main.js +3 -2
  56. package/dist/dev/runner-main.js.map +1 -1
  57. package/dist/dev/runner.d.ts +6 -5
  58. package/dist/dev/runner.d.ts.map +1 -1
  59. package/dist/dev/runner.js +69 -38
  60. package/dist/dev/runner.js.map +1 -1
  61. package/dist/dev/schema-dir.d.ts +2 -1
  62. package/dist/dev/schema-dir.d.ts.map +1 -1
  63. package/dist/dev/schema-dir.js +4 -2
  64. package/dist/dev/schema-dir.js.map +1 -1
  65. package/dist/dev/supervisor.d.ts +7 -6
  66. package/dist/dev/supervisor.d.ts.map +1 -1
  67. package/dist/dev/supervisor.js +107 -67
  68. package/dist/dev/supervisor.js.map +1 -1
  69. package/dist/dev/vite-dev-server.d.ts +3 -3
  70. package/dist/dev/vite-dev-server.d.ts.map +1 -1
  71. package/dist/dev/vite-dev-server.js +2 -1
  72. package/dist/dev/vite-dev-server.js.map +1 -1
  73. package/dist/internal/banner.d.ts +2 -1
  74. package/dist/internal/banner.d.ts.map +1 -1
  75. package/dist/internal/banner.js +2 -1
  76. package/dist/internal/banner.js.map +1 -1
  77. package/dist/internal/data-dir.d.ts +3 -2
  78. package/dist/internal/data-dir.d.ts.map +1 -1
  79. package/dist/internal/data-dir.js +29 -14
  80. package/dist/internal/data-dir.js.map +1 -1
  81. package/dist/internal/entry-arg.d.ts +5 -4
  82. package/dist/internal/entry-arg.d.ts.map +1 -1
  83. package/dist/internal/entry-arg.js +10 -7
  84. package/dist/internal/entry-arg.js.map +1 -1
  85. package/dist/internal/errors.d.ts +3 -2
  86. package/dist/internal/errors.d.ts.map +1 -1
  87. package/dist/internal/errors.js +7 -5
  88. package/dist/internal/errors.js.map +1 -1
  89. package/dist/internal/list-files.d.ts +3 -2
  90. package/dist/internal/list-files.d.ts.map +1 -1
  91. package/dist/internal/list-files.js +5 -3
  92. package/dist/internal/list-files.js.map +1 -1
  93. package/dist/internal/resolve-cli-tool.d.ts +2 -1
  94. package/dist/internal/resolve-cli-tool.d.ts.map +1 -1
  95. package/dist/internal/resolve-cli-tool.js +17 -10
  96. package/dist/internal/resolve-cli-tool.js.map +1 -1
  97. package/dist/internal/staging-dir.d.ts +3 -2
  98. package/dist/internal/staging-dir.d.ts.map +1 -1
  99. package/dist/internal/staging-dir.js +3 -2
  100. package/dist/internal/staging-dir.js.map +1 -1
  101. package/dist/internal/xdg-data-dirs.d.ts +2 -1
  102. package/dist/internal/xdg-data-dirs.d.ts.map +1 -1
  103. package/dist/internal/xdg-data-dirs.js +4 -2
  104. package/dist/internal/xdg-data-dirs.js.map +1 -1
  105. package/dist/mcp/client.d.ts +9 -4
  106. package/dist/mcp/client.d.ts.map +1 -1
  107. package/dist/mcp/client.js +106 -81
  108. package/dist/mcp/client.js.map +1 -1
  109. package/dist/mcp/handlers.d.ts +3 -2
  110. package/dist/mcp/handlers.d.ts.map +1 -1
  111. package/dist/mcp/handlers.js +89 -61
  112. package/dist/mcp/handlers.js.map +1 -1
  113. package/dist/mcp/index.d.ts +2 -2
  114. package/dist/mcp/index.d.ts.map +1 -1
  115. package/dist/mcp/index.js +30 -13
  116. package/dist/mcp/index.js.map +1 -1
  117. package/dist/mcp/serialize-widget.d.ts +3 -3
  118. package/dist/mcp/serialize-widget.d.ts.map +1 -1
  119. package/dist/mcp/serialize-widget.js +9 -6
  120. package/dist/mcp/serialize-widget.js.map +1 -1
  121. package/dist/mcp/testing-loader.d.ts +3 -4
  122. package/dist/mcp/testing-loader.d.ts.map +1 -1
  123. package/dist/mcp/testing-loader.js +43 -21
  124. package/dist/mcp/testing-loader.js.map +1 -1
  125. package/dist/mcp/widget-registry.d.ts +3 -2
  126. package/dist/mcp/widget-registry.d.ts.map +1 -1
  127. package/dist/mcp/widget-registry.js +4 -3
  128. package/dist/mcp/widget-registry.js.map +1 -1
  129. package/dist/refresh-globals.d.ts +2 -0
  130. package/dist/refresh-globals.d.ts.map +1 -0
  131. package/dist/refresh-globals.js +8 -0
  132. package/dist/refresh-globals.js.map +1 -0
  133. package/dist/refresh-runtime.d.ts +5 -11
  134. package/dist/refresh-runtime.d.ts.map +1 -1
  135. package/dist/refresh-runtime.js +16 -14
  136. package/dist/refresh-runtime.js.map +1 -1
  137. package/dist/settings/compile.d.ts +2 -1
  138. package/dist/settings/compile.d.ts.map +1 -1
  139. package/dist/settings/compile.js +6 -4
  140. package/dist/settings/compile.js.map +1 -1
  141. package/dist/settings/parser.d.ts +6 -5
  142. package/dist/settings/parser.d.ts.map +1 -1
  143. package/dist/settings/parser.js +26 -22
  144. package/dist/settings/parser.js.map +1 -1
  145. package/dist/settings/render.d.ts +3 -2
  146. package/dist/settings/render.d.ts.map +1 -1
  147. package/dist/settings/render.js +38 -64
  148. package/dist/settings/render.js.map +1 -1
  149. package/dist/settings/schema.d.ts +8 -8
  150. package/dist/settings/schema.d.ts.map +1 -1
  151. package/dist/settings/schema.js +61 -43
  152. package/dist/settings/schema.js.map +1 -1
  153. package/dist/vite-plugins/asset-extensions.d.ts +4 -3
  154. package/dist/vite-plugins/asset-extensions.d.ts.map +1 -1
  155. package/dist/vite-plugins/asset-extensions.js +4 -3
  156. package/dist/vite-plugins/asset-extensions.js.map +1 -1
  157. package/dist/vite-plugins/built-url.d.ts +2 -1
  158. package/dist/vite-plugins/built-url.d.ts.map +1 -1
  159. package/dist/vite-plugins/built-url.js +19 -13
  160. package/dist/vite-plugins/built-url.js.map +1 -1
  161. package/dist/vite-plugins/css.d.ts +2 -1
  162. package/dist/vite-plugins/css.d.ts.map +1 -1
  163. package/dist/vite-plugins/css.js +23 -15
  164. package/dist/vite-plugins/css.js.map +1 -1
  165. package/dist/vite-plugins/fast-refresh/refresh-filter.d.ts +5 -4
  166. package/dist/vite-plugins/fast-refresh/refresh-filter.d.ts.map +1 -1
  167. package/dist/vite-plugins/fast-refresh/refresh-filter.js +11 -7
  168. package/dist/vite-plugins/fast-refresh/refresh-filter.js.map +1 -1
  169. package/dist/vite-plugins/fast-refresh/swc-refresh.d.ts +4 -3
  170. package/dist/vite-plugins/fast-refresh/swc-refresh.d.ts.map +1 -1
  171. package/dist/vite-plugins/fast-refresh/swc-refresh.js +45 -41
  172. package/dist/vite-plugins/fast-refresh/swc-refresh.js.map +1 -1
  173. package/dist/vite-plugins/icons.d.ts +2 -1
  174. package/dist/vite-plugins/icons.d.ts.map +1 -1
  175. package/dist/vite-plugins/icons.js +24 -20
  176. package/dist/vite-plugins/icons.js.map +1 -1
  177. package/dist/vite-plugins/index.d.ts +2 -1
  178. package/dist/vite-plugins/index.d.ts.map +1 -1
  179. package/dist/vite-plugins/index.js +4 -3
  180. package/dist/vite-plugins/index.js.map +1 -1
  181. package/dist/vite-plugins/native.d.ts +2 -1
  182. package/dist/vite-plugins/native.d.ts.map +1 -1
  183. package/dist/vite-plugins/native.js +6 -4
  184. package/dist/vite-plugins/native.js.map +1 -1
  185. package/dist/vite-plugins/react-compiler.d.ts +3 -2
  186. package/dist/vite-plugins/react-compiler.d.ts.map +1 -1
  187. package/dist/vite-plugins/react-compiler.js +26 -20
  188. package/dist/vite-plugins/react-compiler.js.map +1 -1
  189. package/dist/vite-plugins/react-dom-prebundle.d.ts +2 -1
  190. package/dist/vite-plugins/react-dom-prebundle.d.ts.map +1 -1
  191. package/dist/vite-plugins/react-dom-prebundle.js +2 -1
  192. package/dist/vite-plugins/react-dom-prebundle.js.map +1 -1
  193. package/dist/vite-plugins/resource-init-module.d.ts +2 -1
  194. package/dist/vite-plugins/resource-init-module.d.ts.map +1 -1
  195. package/dist/vite-plugins/resource-init-module.js +39 -38
  196. package/dist/vite-plugins/resource-init-module.js.map +1 -1
  197. package/dist/vite-plugins/resource-shared.d.ts +7 -5
  198. package/dist/vite-plugins/resource-shared.d.ts.map +1 -1
  199. package/dist/vite-plugins/resource-shared.js +17 -10
  200. package/dist/vite-plugins/resource-shared.js.map +1 -1
  201. package/dist/vite-plugins/resources.d.ts +3 -2
  202. package/dist/vite-plugins/resources.d.ts.map +1 -1
  203. package/dist/vite-plugins/resources.js +95 -55
  204. package/dist/vite-plugins/resources.js.map +1 -1
  205. package/dist/vite-plugins/settings-worker-env.d.ts +2 -1
  206. package/dist/vite-plugins/settings-worker-env.d.ts.map +1 -1
  207. package/dist/vite-plugins/settings-worker-env.js +4 -2
  208. package/dist/vite-plugins/settings-worker-env.js.map +1 -1
  209. package/dist/vite-plugins/settings.d.ts +2 -1
  210. package/dist/vite-plugins/settings.d.ts.map +1 -1
  211. package/dist/vite-plugins/settings.js +56 -32
  212. package/dist/vite-plugins/settings.js.map +1 -1
  213. package/dist/vite-plugins/strip-query.d.ts +3 -0
  214. package/dist/vite-plugins/strip-query.d.ts.map +1 -0
  215. package/dist/vite-plugins/strip-query.js +6 -0
  216. package/dist/vite-plugins/strip-query.js.map +1 -0
  217. package/dist/vite-plugins/undeclared-library.d.ts +3 -1
  218. package/dist/vite-plugins/undeclared-library.d.ts.map +1 -1
  219. package/dist/vite-plugins/undeclared-library.js +11 -8
  220. package/dist/vite-plugins/undeclared-library.js.map +1 -1
  221. package/dist/vite-plugins/virtual-module.d.ts +2 -2
  222. package/dist/vite-plugins/virtual-module.d.ts.map +1 -1
  223. package/dist/vite-plugins/virtual-module.js +5 -3
  224. package/dist/vite-plugins/virtual-module.js.map +1 -1
  225. package/dist/vitest-plugin.d.ts.map +1 -1
  226. package/dist/vitest-plugin.js.map +1 -1
  227. package/env.d.ts +21 -61
  228. package/package.json +12 -14
  229. package/src/babel-modules.d.ts +0 -2
  230. package/src/builder.ts +7 -6
  231. package/src/cli.ts +26 -6
  232. package/src/codegen/freshness.ts +39 -26
  233. package/src/codegen/report.ts +10 -2
  234. package/src/codegen/run-codegen.ts +131 -66
  235. package/src/codegen/store-resolver.ts +52 -15
  236. package/src/commands/build.ts +3 -2
  237. package/src/commands/codegen.ts +9 -4
  238. package/src/commands/dev.ts +3 -4
  239. package/src/commands/docs.ts +47 -7
  240. package/src/dev/icon-dir.ts +13 -3
  241. package/src/dev/refresh-tracker.ts +12 -8
  242. package/src/dev/runner-deps.ts +30 -7
  243. package/src/dev/runner-main.ts +4 -3
  244. package/src/dev/runner.ts +100 -41
  245. package/src/dev/schema-dir.ts +7 -2
  246. package/src/dev/supervisor.ts +169 -71
  247. package/src/dev/vite-dev-server.ts +4 -2
  248. package/src/internal/banner.ts +5 -1
  249. package/src/internal/data-dir.ts +47 -16
  250. package/src/internal/entry-arg.ts +16 -8
  251. package/src/internal/errors.ts +15 -7
  252. package/src/internal/list-files.ts +10 -4
  253. package/src/internal/resolve-cli-tool.ts +28 -12
  254. package/src/internal/staging-dir.ts +5 -2
  255. package/src/internal/xdg-data-dirs.ts +8 -2
  256. package/src/mcp/client.ts +143 -85
  257. package/src/mcp/handlers.ts +126 -69
  258. package/src/mcp/index.ts +38 -14
  259. package/src/mcp/serialize-widget.ts +15 -8
  260. package/src/mcp/testing-loader.ts +64 -23
  261. package/src/mcp/widget-registry.ts +11 -5
  262. package/src/refresh-globals.ts +10 -0
  263. package/src/refresh-runtime.ts +20 -16
  264. package/src/settings/compile.ts +7 -4
  265. package/src/settings/parser.ts +69 -30
  266. package/src/settings/render.ts +66 -66
  267. package/src/settings/schema.ts +100 -46
  268. package/src/vite-plugins/asset-extensions.ts +4 -3
  269. package/src/vite-plugins/built-url.ts +30 -15
  270. package/src/vite-plugins/css.ts +35 -20
  271. package/src/vite-plugins/fast-refresh/refresh-filter.ts +18 -10
  272. package/src/vite-plugins/fast-refresh/swc-refresh.ts +60 -47
  273. package/src/vite-plugins/icons.ts +40 -24
  274. package/src/vite-plugins/index.ts +7 -4
  275. package/src/vite-plugins/native.ts +14 -5
  276. package/src/vite-plugins/react-compiler.ts +44 -30
  277. package/src/vite-plugins/react-dom-prebundle.ts +4 -1
  278. package/src/vite-plugins/resource-init-module.ts +41 -38
  279. package/src/vite-plugins/resource-shared.ts +27 -14
  280. package/src/vite-plugins/resources.ts +154 -60
  281. package/src/vite-plugins/settings-worker-env.ts +7 -2
  282. package/src/vite-plugins/settings.ts +107 -42
  283. package/src/vite-plugins/strip-query.ts +7 -0
  284. package/src/vite-plugins/undeclared-library.ts +26 -16
  285. package/src/vite-plugins/virtual-module.ts +9 -2
  286. package/src/vitest-plugin.ts +1 -2
package/src/mcp/client.ts CHANGED
@@ -1,20 +1,58 @@
1
- import * as net from "node:net";
2
1
  import * as Gio from "@gtkx/gi/gio";
3
2
  import * as Gtk from "@gtkx/gi/gtk";
4
3
  import { DEFAULT_SOCKET_PATH, ErrorCode, ProtocolConnection, ProtocolError, type Request } from "@gtkx/mcp/internal";
5
4
  import { error, errorMessage, info, normalizeError, warn } from "@gtkx/utils";
5
+ import * as net from "node:net";
6
6
  import { dispatch } from "./handlers.js";
7
7
  import { WidgetRegistry } from "./widget-registry.js";
8
8
 
9
- export type McpClientOptions = {
9
+ type McpClientOptions = {
10
10
  socketPath?: string;
11
11
  applicationId: string;
12
12
  };
13
13
 
14
+ type ConnectCallbacks = {
15
+ onSuccess?: (() => void) | undefined;
16
+ onError?: ((error: Error) => void) | undefined;
17
+ };
18
+
19
+ type ConnectSettler = {
20
+ succeed: () => void;
21
+ fail: (error: Error) => void;
22
+ };
23
+
14
24
  const RECONNECT_DELAY_MS = 2000;
15
- const REGISTER_TIMEOUT_MS = 30000;
25
+ const REGISTER_TIMEOUT_MS = 30_000;
26
+ const DISCONNECT_ERROR_CODES: Set<string> = new Set(["ENOENT", "ECONNREFUSED", "EPIPE", "ECONNRESET"]);
27
+
28
+ const toResponseError = (error: unknown): { code: number; message: string; data?: unknown } =>
29
+ error instanceof ProtocolError
30
+ ? error.toErrorObject()
31
+ : { code: ErrorCode.INTERNAL_ERROR, message: errorMessage(error) };
32
+
33
+ const connectSettler = (callbacks: ConnectCallbacks): ConnectSettler => {
34
+ let isSettled = false;
35
+
36
+ const settle = (notify: () => void): void => {
37
+ if (isSettled) {
38
+ return;
39
+ }
40
+
41
+ isSettled = true;
42
+ notify();
43
+ };
44
+
45
+ return {
46
+ succeed: () => {
47
+ settle(() => callbacks.onSuccess?.());
48
+ },
49
+ fail: (failure) => {
50
+ settle(() => callbacks.onError?.(failure));
51
+ },
52
+ };
53
+ };
16
54
 
17
- export class McpClient {
55
+ class McpClient {
18
56
  private socket: net.Socket | null = null;
19
57
  private connection: ProtocolConnection | null = null;
20
58
  private socketPath: string;
@@ -30,92 +68,61 @@ export class McpClient {
30
68
  this.applicationId = options.applicationId;
31
69
  }
32
70
 
33
- async connect(): Promise<void> {
34
- return new Promise<void>((resolve, reject) => {
35
- this.pendingConnectReject = reject;
36
- this.attemptConnect(
37
- () => {
38
- this.pendingConnectReject = null;
39
- resolve();
40
- },
41
- (error) => {
42
- this.pendingConnectReject = null;
43
- reject(error);
44
- },
45
- );
46
- });
71
+ private handleClose(): void {
72
+ if (this.hasConnected) {
73
+ info("Disconnected from MCP server");
74
+ this.hasConnected = false;
75
+ }
76
+
77
+ this.socket = null;
78
+ this.connection = null;
79
+ this.scheduleReconnect();
47
80
  }
48
81
 
49
- disconnect(): void {
50
- this.isStopping = true;
51
- if (this.pendingConnectReject) {
52
- this.pendingConnectReject(new Error("Client disconnected before connection registered"));
53
- this.pendingConnectReject = null;
54
- }
55
- if (this.reconnectTimer) {
56
- clearTimeout(this.reconnectTimer);
57
- this.reconnectTimer = null;
58
- }
59
- this.connection?.rejectPending(new Error("Client disconnected"));
60
- if (this.socket) {
61
- this.connection?.write({ id: crypto.randomUUID(), method: "app.unregister" });
62
- this.socket.destroy();
63
- this.socket = null;
82
+ private handleSocketError(socketError: Error): void {
83
+ const code = (socketError as NodeJS.ErrnoException).code;
84
+
85
+ if (code !== undefined && DISCONNECT_ERROR_CODES.has(code)) {
86
+ this.scheduleReconnect();
87
+ } else {
88
+ error("Socket error:", socketError.message);
64
89
  }
65
- this.connection = null;
66
- this.hasConnected = false;
67
90
  }
68
91
 
69
- private attemptConnect(onSuccess?: () => void, onError?: (error: Error) => void): void {
70
- let settled = false;
92
+ private async registerWithServer(settle: ConnectSettler): Promise<void> {
93
+ try {
94
+ await this.register();
95
+ info("Registered with MCP server");
96
+ settle.succeed();
97
+ } catch (registerError) {
98
+ error("Failed to register with MCP server:", errorMessage(registerError));
99
+ settle.fail(normalizeError(registerError));
100
+ }
101
+ }
71
102
 
72
- const settle = <T extends unknown[]>(callback: ((...args: T) => void) | undefined, ...args: T) => {
73
- if (settled) return;
74
- settled = true;
75
- callback?.(...args);
76
- };
103
+ private attemptConnect(callbacks: ConnectCallbacks = {}): void {
104
+ const settle = connectSettler(callbacks);
77
105
 
78
106
  const socket = net.createConnection(this.socketPath, () => {
79
107
  info(`Connected to MCP server at ${this.socketPath}`);
80
108
  this.hasConnected = true;
81
- this.register()
82
- .then(() => {
83
- info("Registered with MCP server");
84
- settle(onSuccess);
85
- })
86
- .catch((cause) => {
87
- error("Failed to register with MCP server:", cause.message);
88
- settle(onError, normalizeError(cause));
89
- });
109
+ void this.registerWithServer(settle);
90
110
  });
91
111
 
92
112
  const connection = ProtocolConnection.fromSocket(socket, {
93
113
  onClose: () => {
94
- if (this.hasConnected) {
95
- info("Disconnected from MCP server");
96
- this.hasConnected = false;
97
- }
98
- this.socket = null;
99
- this.connection = null;
100
- this.scheduleReconnect();
114
+ this.handleClose();
101
115
  },
102
116
  onError: (socketError) => {
103
- const code = (socketError as NodeJS.ErrnoException).code;
104
- const isDisconnectError =
105
- code === "ENOENT" || code === "ECONNREFUSED" || code === "EPIPE" || code === "ECONNRESET";
106
- if (isDisconnectError) {
107
- this.scheduleReconnect();
108
- } else {
109
- error("Socket error:", socketError.message);
110
- }
111
- settle(onError, socketError);
117
+ this.handleSocketError(socketError);
118
+ settle.fail(socketError);
112
119
  },
113
120
  });
121
+
114
122
  connection.on("request", (request) => {
115
- this.handleRequest(request).catch((cause) => {
116
- error("Error handling request:", cause);
117
- });
123
+ void this.handleRequest(request);
118
124
  });
125
+
119
126
  connection.on("invalid", ({ error: parseError }) => {
120
127
  warn(`Received invalid JSON from MCP server: ${parseError.message}`);
121
128
  });
@@ -125,7 +132,10 @@ export class McpClient {
125
132
  }
126
133
 
127
134
  private scheduleReconnect(): void {
128
- if (this.reconnectTimer || this.isStopping) return;
135
+ if (this.reconnectTimer || this.isStopping) {
136
+ return;
137
+ }
138
+
129
139
  this.reconnectTimer = setTimeout(() => {
130
140
  this.reconnectTimer = null;
131
141
  this.attemptConnect();
@@ -136,6 +146,7 @@ export class McpClient {
136
146
  if (!this.connection) {
137
147
  return Promise.reject(new Error("Connection not initialized"));
138
148
  }
149
+
139
150
  return this.connection.send(
140
151
  "app.register",
141
152
  {
@@ -147,31 +158,78 @@ export class McpClient {
147
158
  );
148
159
  }
149
160
 
150
- private async handleRequest(request: Request): Promise<void> {
161
+ private async respondToRequest(request: Request): Promise<void> {
151
162
  const { id, method, params } = request;
152
163
  const connection = this.connection;
153
- if (!connection) return;
164
+
165
+ if (!connection) {
166
+ return;
167
+ }
154
168
 
155
169
  try {
156
170
  const defaultApp = Gio.Application.getDefault();
171
+
157
172
  if (!(defaultApp instanceof Gtk.Application)) {
158
173
  throw new TypeError("Application not initialized");
159
174
  }
175
+
160
176
  this.registry.refresh();
161
177
  const result = await dispatch(method, params, { app: defaultApp, registry: this.registry });
162
178
  connection.write({ id, result });
163
- } catch (error) {
164
- if (error instanceof ProtocolError) {
165
- connection.write({ id, error: error.toErrorObject() });
166
- } else {
167
- connection.write({
168
- id,
169
- error: {
170
- code: ErrorCode.INTERNAL_ERROR,
171
- message: errorMessage(error),
172
- },
173
- });
174
- }
179
+ } catch (dispatchError) {
180
+ connection.write({ id, error: toResponseError(dispatchError) });
175
181
  }
176
182
  }
183
+
184
+ private async handleRequest(request: Request): Promise<void> {
185
+ try {
186
+ await this.respondToRequest(request);
187
+ } catch (requestError) {
188
+ error("Error handling request:", requestError);
189
+ }
190
+ }
191
+
192
+ async connect(): Promise<void> {
193
+ return new Promise<void>((resolve, reject) => {
194
+ this.pendingConnectReject = reject;
195
+
196
+ this.attemptConnect({
197
+ onSuccess: () => {
198
+ this.pendingConnectReject = null;
199
+ resolve();
200
+ },
201
+ onError: (connectError) => {
202
+ this.pendingConnectReject = null;
203
+ reject(connectError);
204
+ },
205
+ });
206
+ });
207
+ }
208
+
209
+ disconnect(): void {
210
+ this.isStopping = true;
211
+
212
+ if (this.pendingConnectReject) {
213
+ this.pendingConnectReject(new Error("Client disconnected before connection registered"));
214
+ this.pendingConnectReject = null;
215
+ }
216
+
217
+ if (this.reconnectTimer) {
218
+ clearTimeout(this.reconnectTimer);
219
+ this.reconnectTimer = null;
220
+ }
221
+
222
+ this.connection?.rejectPending(new Error("Client disconnected"));
223
+
224
+ if (this.socket) {
225
+ this.connection?.write({ id: crypto.randomUUID(), method: "app.unregister" });
226
+ this.socket.destroy();
227
+ this.socket = null;
228
+ }
229
+
230
+ this.connection = null;
231
+ this.hasConnected = false;
232
+ }
177
233
  }
234
+
235
+ export { McpClient, type McpClientOptions };
@@ -1,5 +1,3 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
2
- import { dirname } from "node:path";
3
1
  import * as Gtk from "@gtkx/gi/gtk";
4
2
  import {
5
3
  invalidRequestError,
@@ -10,147 +8,206 @@ import {
10
8
  ServerRequestParamsSchemas,
11
9
  widgetNotFoundError,
12
10
  } from "@gtkx/mcp/internal";
13
- import { serializeWidget } from "./serialize-widget.js";
14
- import { loadTestingModule } from "./testing-loader.js";
11
+ import { mkdirSync, writeFileSync } from "node:fs";
12
+ import { dirname } from "node:path";
15
13
  import type { WidgetRegistry } from "./widget-registry.js";
14
+ import { serializeWidget } from "./serialize-widget.js";
15
+ import { loadTestingModule, type TestingModule } from "./testing-loader.js";
16
16
 
17
- export type HandlerContext = {
17
+ type HandlerContext = {
18
18
  app: Gtk.Application;
19
19
  registry: WidgetRegistry;
20
20
  };
21
21
 
22
22
  type ValidatedHandler = (ctx: HandlerContext, params: unknown) => Promise<unknown>;
23
+ type WidgetTarget = { testing: TestingModule; widget: Gtk.Widget };
23
24
 
24
- const validated = <Params>(
25
+ const HANDLERS: Record<ServerInitiatedMethod, ValidatedHandler> = {
26
+ "app.getWindows": validated(ServerRequestParamsSchemas["app.getWindows"], ({ registry }) =>
27
+ Promise.resolve({
28
+ windows: registry.toplevels().map((window) => ({
29
+ id: registry.getOrCreateId(window),
30
+ title: window.getTitle(),
31
+ })),
32
+ })),
33
+ "widget.getTree": validated(ServerRequestParamsSchemas["widget.getTree"], async ({ app, registry }, params) => {
34
+ const testing = await loadTestingModule();
35
+ const container = params.rootId === undefined ? app : requireWidget(registry, params.rootId);
36
+
37
+ return {
38
+ tree: testing.prettyWidget(container, {
39
+ getId: (w) => registry.getOrCreateId(w),
40
+ highlight: false,
41
+ ...((params.maxDepth !== undefined) && { maxDepth: params.maxDepth }),
42
+ }),
43
+ };
44
+ }),
45
+ "widget.query": validated(ServerRequestParamsSchemas["widget.query"], handleQuery),
46
+ "widget.getProps": validated(ServerRequestParamsSchemas["widget.getProps"], async ({ registry }, params) => {
47
+ const { testing, widget } = await widgetTarget(registry, params.widgetId);
48
+
49
+ return serializeWidget(widget, (target) => registry.getOrCreateId(target), testing);
50
+ }),
51
+ "widget.click": validated(ServerRequestParamsSchemas["widget.click"], async ({ registry }, params) => {
52
+ const { testing, widget } = await widgetTarget(registry, params.widgetId);
53
+ await testing.userEvent.click(widget);
54
+
55
+ return { success: true };
56
+ }),
57
+ "widget.type": validated(ServerRequestParamsSchemas["widget.type"], async ({ registry }, params) => {
58
+ const { testing, widget } = await widgetTarget(registry, params.widgetId);
59
+
60
+ if (params.clear) {
61
+ await testing.userEvent.clear(widget);
62
+ }
63
+
64
+ await testing.userEvent.type(widget, params.text);
65
+
66
+ return { success: true };
67
+ }),
68
+ "widget.fireEvent": validated(ServerRequestParamsSchemas["widget.fireEvent"], async ({ registry }, params) => {
69
+ const { testing, widget } = await widgetTarget(registry, params.widgetId);
70
+ const signalArgs = (params.args ?? []).map((arg) => extractSignalArg(arg));
71
+ await testing.fireEvent(widget, params.signal, ...signalArgs);
72
+
73
+ return { success: true };
74
+ }),
75
+ "widget.screenshot": validated(ServerRequestParamsSchemas["widget.screenshot"], handleScreenshot),
76
+ };
77
+
78
+ function validated<Params>(
25
79
  schema: ParamsSchema<Params>,
26
80
  handler: (ctx: HandlerContext, params: Params) => Promise<unknown>,
27
- ): ValidatedHandler => {
81
+ ): ValidatedHandler {
28
82
  return (ctx, params) => {
29
83
  const parsed = schema.safeParse(params ?? {});
84
+
30
85
  if (!parsed.success) {
31
86
  throw invalidRequestError(parsed.error.message);
32
87
  }
88
+
33
89
  return handler(ctx, parsed.data);
34
90
  };
35
- };
91
+ }
92
+
93
+ const widgetTarget = async (registry: WidgetRegistry, widgetId: string | undefined): Promise<WidgetTarget> => ({
94
+ testing: await loadTestingModule(),
95
+ widget: requireWidget(registry, widgetId),
96
+ });
36
97
 
37
98
  const requireWidget = (registry: WidgetRegistry, widgetId: string | undefined): Gtk.Widget => {
38
99
  if (widgetId === undefined) {
39
100
  throw widgetNotFoundError("undefined");
40
101
  }
102
+
41
103
  const widget = registry.get(widgetId);
104
+
42
105
  if (!widget) {
43
106
  throw widgetNotFoundError(widgetId);
44
107
  }
108
+
45
109
  return widget;
46
110
  };
47
111
 
48
112
  const extractSignalArg = (arg: unknown): unknown => {
49
113
  const isTypedArg = typeof arg === "object" && arg !== null && "type" in arg && "value" in arg;
114
+
50
115
  return isTypedArg ? (arg as { value: unknown }).value : arg;
51
116
  };
52
117
 
53
- const handleQuery = async (
118
+ const resolveRole = (value: string | number): Gtk.AccessibleRole | undefined => {
119
+ if (typeof value === "number") {
120
+ return value;
121
+ }
122
+
123
+ const resolved = Gtk.AccessibleRole[value.toUpperCase() as keyof typeof Gtk.AccessibleRole];
124
+
125
+ return typeof resolved === "number" ? resolved : undefined;
126
+ };
127
+
128
+ const matchesOrEmpty = async (find: () => Promise<Gtk.Widget[]>): Promise<Gtk.Widget[]> => {
129
+ try {
130
+ return await find();
131
+ } catch {
132
+ return [];
133
+ }
134
+ };
135
+
136
+ async function handleQuery(
54
137
  { app, registry }: HandlerContext,
55
138
  params: ServerRequestParams<"widget.query">,
56
- ): Promise<unknown> => {
139
+ ): Promise<unknown> {
57
140
  const testing = await loadTestingModule();
58
141
  let widgets: Gtk.Widget[] = [];
59
142
 
60
143
  switch (params.by) {
61
144
  case "role": {
62
- const roleValue =
63
- typeof params.value === "string"
64
- ? Gtk.AccessibleRole[params.value as keyof typeof Gtk.AccessibleRole]
65
- : params.value;
66
- widgets = await testing.findAllByRole(app, roleValue as Gtk.AccessibleRole, params.options);
145
+ const roleValue = resolveRole(params.value);
146
+
147
+ if (roleValue === undefined) {
148
+ throw invalidRequestError(
149
+ `Unknown accessible role "${String(params.value)}"; use the lowercase role shown in the ` +
150
+ "widget tree, e.g. \"button\", \"list\", \"list_item\", or \"checkbox\".",
151
+ );
152
+ }
153
+
154
+ widgets = await matchesOrEmpty(() => testing.findAllByRole(app, roleValue, params.options));
67
155
  break;
68
156
  }
69
- case "text":
70
- widgets = await testing.findAllByText(app, String(params.value), params.options);
157
+ case "text": {
158
+ widgets = await matchesOrEmpty(() => testing.findAllByText(app, String(params.value), params.options));
71
159
  break;
72
- case "name":
73
- widgets = await testing.findAllByName(app, String(params.value), params.options);
160
+ }
161
+ case "name": {
162
+ widgets = await matchesOrEmpty(() => testing.findAllByName(app, String(params.value), params.options));
74
163
  break;
75
- case "labelText":
76
- widgets = await testing.findAllByLabelText(app, String(params.value), params.options);
164
+ }
165
+ case "labelText": {
166
+ widgets = await matchesOrEmpty(() => testing.findAllByLabelText(app, String(params.value), params.options));
77
167
  break;
168
+ }
78
169
  }
79
170
 
80
- return { widgets: widgets.map((w) => serializeWidget(w, (widget) => registry.idFor(widget), testing)) };
81
- };
171
+ return { widgets: widgets.map((w) => serializeWidget(w, (widget) => registry.getOrCreateId(widget), testing, 0)) };
172
+ }
82
173
 
83
174
  const defaultScreenshotTarget = (registry: WidgetRegistry): Gtk.Widget => {
84
175
  const [toplevel] = registry.toplevels();
176
+
85
177
  if (!toplevel) {
86
178
  throw new Error("No windows available for screenshot");
87
179
  }
180
+
88
181
  return toplevel;
89
182
  };
90
183
 
91
- const handleScreenshot = async (
184
+ async function handleScreenshot(
92
185
  { registry }: HandlerContext,
93
186
  params: ServerRequestParams<"widget.screenshot">,
94
- ): Promise<unknown> => {
187
+ ): Promise<unknown> {
95
188
  const testing = await loadTestingModule();
96
189
  const target = params.windowId ? requireWidget(registry, params.windowId) : defaultScreenshotTarget(registry);
97
190
  const result = await testing.screenshot(target);
191
+
98
192
  if (params.path) {
99
193
  mkdirSync(dirname(params.path), { recursive: true });
100
194
  writeFileSync(params.path, Buffer.from(result.data, "base64"));
195
+
101
196
  return { data: result.data, mimeType: result.mimeType, savedPath: params.path };
102
197
  }
103
- return { data: result.data, mimeType: result.mimeType };
104
- };
105
198
 
106
- const HANDLERS: Record<ServerInitiatedMethod, ValidatedHandler> = {
107
- "app.getWindows": validated(ServerRequestParamsSchemas["app.getWindows"], async ({ registry }) => ({
108
- windows: registry.toplevels().map((window) => ({
109
- id: registry.idFor(window),
110
- title: window.getTitle(),
111
- })),
112
- })),
113
- "widget.getTree": validated(ServerRequestParamsSchemas["widget.getTree"], async ({ app, registry }) => {
114
- const testing = await loadTestingModule();
115
- return { tree: testing.prettyWidget(app, { getId: (w) => registry.idFor(w), highlight: false }) };
116
- }),
117
- "widget.query": validated(ServerRequestParamsSchemas["widget.query"], handleQuery),
118
- "widget.getProps": validated(ServerRequestParamsSchemas["widget.getProps"], async ({ registry }, params) => {
119
- const testing = await loadTestingModule();
120
- const widget = requireWidget(registry, params.widgetId);
121
- return serializeWidget(widget, (target) => registry.idFor(target), testing);
122
- }),
123
- "widget.click": validated(ServerRequestParamsSchemas["widget.click"], async ({ registry }, params) => {
124
- const testing = await loadTestingModule();
125
- const widget = requireWidget(registry, params.widgetId);
126
- await testing.userEvent.click(widget);
127
- return { success: true };
128
- }),
129
- "widget.type": validated(ServerRequestParamsSchemas["widget.type"], async ({ registry }, params) => {
130
- const testing = await loadTestingModule();
131
- const widget = requireWidget(registry, params.widgetId);
132
- if (params.clear) {
133
- await testing.userEvent.clear(widget);
134
- }
135
- await testing.userEvent.type(widget, params.text);
136
- return { success: true };
137
- }),
138
- "widget.fireEvent": validated(ServerRequestParamsSchemas["widget.fireEvent"], async ({ registry }, params) => {
139
- const testing = await loadTestingModule();
140
- const widget = requireWidget(registry, params.widgetId);
141
- const signalArgs = (params.args ?? []).map(extractSignalArg);
142
- await testing.fireEvent(widget, params.signal, ...signalArgs);
143
- return { success: true };
144
- }),
145
- "widget.screenshot": validated(ServerRequestParamsSchemas["widget.screenshot"], handleScreenshot),
146
- };
199
+ return { data: result.data, mimeType: result.mimeType };
200
+ }
147
201
 
148
202
  const isServerInitiatedMethod = (method: string): method is ServerInitiatedMethod =>
149
203
  Object.hasOwn(ServerRequestParamsSchemas, method);
150
204
 
151
- export const dispatch = async (method: string, params: unknown, ctx: HandlerContext): Promise<unknown> => {
205
+ const dispatch = async (method: string, params: unknown, ctx: HandlerContext): Promise<unknown> => {
152
206
  if (!isServerInitiatedMethod(method)) {
153
207
  throw methodNotFoundError(method);
154
208
  }
209
+
155
210
  return HANDLERS[method](ctx, params);
156
211
  };
212
+
213
+ export { dispatch, type HandlerContext };
package/src/mcp/index.ts CHANGED
@@ -1,22 +1,46 @@
1
1
  import { McpClient } from "./client.js";
2
2
 
3
- let globalClient: McpClient | null = null;
3
+ type McpClientController = {
4
+ start: (applicationId: string) => Promise<McpClient>;
5
+ stop: () => void;
6
+ };
7
+
8
+ const { start: startMcpClient, stop: stopMcpClient } = createMcpClientController();
4
9
 
5
- export const startMcpClient = async (applicationId: string): Promise<McpClient> => {
6
- if (globalClient) {
7
- return globalClient;
10
+ async function connectQuietly(client: McpClient): Promise<boolean> {
11
+ try {
12
+ await client.connect();
13
+
14
+ return true;
15
+ } catch {
16
+ return false;
8
17
  }
18
+ }
9
19
 
10
- globalClient = new McpClient({ applicationId });
20
+ function createMcpClientController(): McpClientController {
21
+ let current: McpClient | null = null;
11
22
 
12
- await globalClient.connect().catch(() => {});
23
+ return {
24
+ start: async (applicationId: string): Promise<McpClient> => {
25
+ if (current) {
26
+ return current;
27
+ }
13
28
 
14
- return globalClient;
15
- };
29
+ const client = new McpClient({ applicationId });
30
+ current = client;
31
+ await connectQuietly(client);
16
32
 
17
- export const stopMcpClient = (): void => {
18
- if (globalClient) {
19
- globalClient.disconnect();
20
- globalClient = null;
21
- }
22
- };
33
+ return client;
34
+ },
35
+ stop: (): void => {
36
+ if (!current) {
37
+ return;
38
+ }
39
+
40
+ current.disconnect();
41
+ current = null;
42
+ },
43
+ };
44
+ }
45
+
46
+ export { startMcpClient, stopMcpClient };