@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.
- package/dist/builder.d.ts +3 -2
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +2 -1
- package/dist/builder.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +21 -6
- package/dist/cli.js.map +1 -1
- package/dist/codegen/freshness.d.ts +5 -5
- package/dist/codegen/freshness.d.ts.map +1 -1
- package/dist/codegen/freshness.js +26 -23
- package/dist/codegen/freshness.js.map +1 -1
- package/dist/codegen/report.d.ts +2 -1
- package/dist/codegen/report.d.ts.map +1 -1
- package/dist/codegen/report.js +4 -2
- package/dist/codegen/report.js.map +1 -1
- package/dist/codegen/run-codegen.d.ts +10 -10
- package/dist/codegen/run-codegen.d.ts.map +1 -1
- package/dist/codegen/run-codegen.js +72 -47
- package/dist/codegen/run-codegen.js.map +1 -1
- package/dist/codegen/store-resolver.d.ts +9 -6
- package/dist/codegen/store-resolver.d.ts.map +1 -1
- package/dist/codegen/store-resolver.js +24 -7
- package/dist/codegen/store-resolver.js.map +1 -1
- package/dist/commands/build.d.ts +4 -3
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +2 -1
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/codegen.d.ts +2 -1
- package/dist/commands/codegen.d.ts.map +1 -1
- package/dist/commands/codegen.js +6 -4
- package/dist/commands/codegen.js.map +1 -1
- package/dist/commands/dev.d.ts +2 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +2 -1
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/docs.d.ts +2 -1
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +25 -7
- package/dist/commands/docs.js.map +1 -1
- package/dist/dev/icon-dir.d.ts +2 -1
- package/dist/dev/icon-dir.d.ts.map +1 -1
- package/dist/dev/icon-dir.js +6 -3
- package/dist/dev/icon-dir.js.map +1 -1
- package/dist/dev/refresh-tracker.d.ts +5 -4
- package/dist/dev/refresh-tracker.d.ts.map +1 -1
- package/dist/dev/refresh-tracker.js +6 -5
- package/dist/dev/refresh-tracker.js.map +1 -1
- package/dist/dev/runner-deps.d.ts +2 -1
- package/dist/dev/runner-deps.d.ts.map +1 -1
- package/dist/dev/runner-deps.js +20 -4
- package/dist/dev/runner-deps.js.map +1 -1
- package/dist/dev/runner-main.d.ts +2 -1
- package/dist/dev/runner-main.d.ts.map +1 -1
- package/dist/dev/runner-main.js +3 -2
- package/dist/dev/runner-main.js.map +1 -1
- package/dist/dev/runner.d.ts +6 -5
- package/dist/dev/runner.d.ts.map +1 -1
- package/dist/dev/runner.js +69 -38
- package/dist/dev/runner.js.map +1 -1
- package/dist/dev/schema-dir.d.ts +2 -1
- package/dist/dev/schema-dir.d.ts.map +1 -1
- package/dist/dev/schema-dir.js +4 -2
- package/dist/dev/schema-dir.js.map +1 -1
- package/dist/dev/supervisor.d.ts +7 -6
- package/dist/dev/supervisor.d.ts.map +1 -1
- package/dist/dev/supervisor.js +107 -67
- package/dist/dev/supervisor.js.map +1 -1
- package/dist/dev/vite-dev-server.d.ts +3 -3
- package/dist/dev/vite-dev-server.d.ts.map +1 -1
- package/dist/dev/vite-dev-server.js +2 -1
- package/dist/dev/vite-dev-server.js.map +1 -1
- package/dist/internal/banner.d.ts +2 -1
- package/dist/internal/banner.d.ts.map +1 -1
- package/dist/internal/banner.js +2 -1
- package/dist/internal/banner.js.map +1 -1
- package/dist/internal/data-dir.d.ts +3 -2
- package/dist/internal/data-dir.d.ts.map +1 -1
- package/dist/internal/data-dir.js +29 -14
- package/dist/internal/data-dir.js.map +1 -1
- package/dist/internal/entry-arg.d.ts +5 -4
- package/dist/internal/entry-arg.d.ts.map +1 -1
- package/dist/internal/entry-arg.js +10 -7
- package/dist/internal/entry-arg.js.map +1 -1
- package/dist/internal/errors.d.ts +3 -2
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/errors.js +7 -5
- package/dist/internal/errors.js.map +1 -1
- package/dist/internal/list-files.d.ts +3 -2
- package/dist/internal/list-files.d.ts.map +1 -1
- package/dist/internal/list-files.js +5 -3
- package/dist/internal/list-files.js.map +1 -1
- package/dist/internal/resolve-cli-tool.d.ts +2 -1
- package/dist/internal/resolve-cli-tool.d.ts.map +1 -1
- package/dist/internal/resolve-cli-tool.js +17 -10
- package/dist/internal/resolve-cli-tool.js.map +1 -1
- package/dist/internal/staging-dir.d.ts +3 -2
- package/dist/internal/staging-dir.d.ts.map +1 -1
- package/dist/internal/staging-dir.js +3 -2
- package/dist/internal/staging-dir.js.map +1 -1
- package/dist/internal/xdg-data-dirs.d.ts +2 -1
- package/dist/internal/xdg-data-dirs.d.ts.map +1 -1
- package/dist/internal/xdg-data-dirs.js +4 -2
- package/dist/internal/xdg-data-dirs.js.map +1 -1
- package/dist/mcp/client.d.ts +9 -4
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +106 -81
- package/dist/mcp/client.js.map +1 -1
- package/dist/mcp/handlers.d.ts +3 -2
- package/dist/mcp/handlers.d.ts.map +1 -1
- package/dist/mcp/handlers.js +89 -61
- package/dist/mcp/handlers.js.map +1 -1
- package/dist/mcp/index.d.ts +2 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +30 -13
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/serialize-widget.d.ts +3 -3
- package/dist/mcp/serialize-widget.d.ts.map +1 -1
- package/dist/mcp/serialize-widget.js +9 -6
- package/dist/mcp/serialize-widget.js.map +1 -1
- package/dist/mcp/testing-loader.d.ts +3 -4
- package/dist/mcp/testing-loader.d.ts.map +1 -1
- package/dist/mcp/testing-loader.js +43 -21
- package/dist/mcp/testing-loader.js.map +1 -1
- package/dist/mcp/widget-registry.d.ts +3 -2
- package/dist/mcp/widget-registry.d.ts.map +1 -1
- package/dist/mcp/widget-registry.js +4 -3
- package/dist/mcp/widget-registry.js.map +1 -1
- package/dist/refresh-globals.d.ts +2 -0
- package/dist/refresh-globals.d.ts.map +1 -0
- package/dist/refresh-globals.js +8 -0
- package/dist/refresh-globals.js.map +1 -0
- package/dist/refresh-runtime.d.ts +5 -11
- package/dist/refresh-runtime.d.ts.map +1 -1
- package/dist/refresh-runtime.js +16 -14
- package/dist/refresh-runtime.js.map +1 -1
- package/dist/settings/compile.d.ts +2 -1
- package/dist/settings/compile.d.ts.map +1 -1
- package/dist/settings/compile.js +6 -4
- package/dist/settings/compile.js.map +1 -1
- package/dist/settings/parser.d.ts +6 -5
- package/dist/settings/parser.d.ts.map +1 -1
- package/dist/settings/parser.js +26 -22
- package/dist/settings/parser.js.map +1 -1
- package/dist/settings/render.d.ts +3 -2
- package/dist/settings/render.d.ts.map +1 -1
- package/dist/settings/render.js +38 -64
- package/dist/settings/render.js.map +1 -1
- package/dist/settings/schema.d.ts +8 -8
- package/dist/settings/schema.d.ts.map +1 -1
- package/dist/settings/schema.js +61 -43
- package/dist/settings/schema.js.map +1 -1
- package/dist/vite-plugins/asset-extensions.d.ts +4 -3
- package/dist/vite-plugins/asset-extensions.d.ts.map +1 -1
- package/dist/vite-plugins/asset-extensions.js +4 -3
- package/dist/vite-plugins/asset-extensions.js.map +1 -1
- package/dist/vite-plugins/built-url.d.ts +2 -1
- package/dist/vite-plugins/built-url.d.ts.map +1 -1
- package/dist/vite-plugins/built-url.js +19 -13
- package/dist/vite-plugins/built-url.js.map +1 -1
- package/dist/vite-plugins/css.d.ts +2 -1
- package/dist/vite-plugins/css.d.ts.map +1 -1
- package/dist/vite-plugins/css.js +23 -15
- package/dist/vite-plugins/css.js.map +1 -1
- package/dist/vite-plugins/fast-refresh/refresh-filter.d.ts +5 -4
- package/dist/vite-plugins/fast-refresh/refresh-filter.d.ts.map +1 -1
- package/dist/vite-plugins/fast-refresh/refresh-filter.js +11 -7
- package/dist/vite-plugins/fast-refresh/refresh-filter.js.map +1 -1
- package/dist/vite-plugins/fast-refresh/swc-refresh.d.ts +4 -3
- package/dist/vite-plugins/fast-refresh/swc-refresh.d.ts.map +1 -1
- package/dist/vite-plugins/fast-refresh/swc-refresh.js +45 -41
- package/dist/vite-plugins/fast-refresh/swc-refresh.js.map +1 -1
- package/dist/vite-plugins/icons.d.ts +2 -1
- package/dist/vite-plugins/icons.d.ts.map +1 -1
- package/dist/vite-plugins/icons.js +24 -20
- package/dist/vite-plugins/icons.js.map +1 -1
- package/dist/vite-plugins/index.d.ts +2 -1
- package/dist/vite-plugins/index.d.ts.map +1 -1
- package/dist/vite-plugins/index.js +4 -3
- package/dist/vite-plugins/index.js.map +1 -1
- package/dist/vite-plugins/native.d.ts +2 -1
- package/dist/vite-plugins/native.d.ts.map +1 -1
- package/dist/vite-plugins/native.js +6 -4
- package/dist/vite-plugins/native.js.map +1 -1
- package/dist/vite-plugins/react-compiler.d.ts +3 -2
- package/dist/vite-plugins/react-compiler.d.ts.map +1 -1
- package/dist/vite-plugins/react-compiler.js +26 -20
- package/dist/vite-plugins/react-compiler.js.map +1 -1
- package/dist/vite-plugins/react-dom-prebundle.d.ts +2 -1
- package/dist/vite-plugins/react-dom-prebundle.d.ts.map +1 -1
- package/dist/vite-plugins/react-dom-prebundle.js +2 -1
- package/dist/vite-plugins/react-dom-prebundle.js.map +1 -1
- package/dist/vite-plugins/resource-init-module.d.ts +2 -1
- package/dist/vite-plugins/resource-init-module.d.ts.map +1 -1
- package/dist/vite-plugins/resource-init-module.js +39 -38
- package/dist/vite-plugins/resource-init-module.js.map +1 -1
- package/dist/vite-plugins/resource-shared.d.ts +7 -5
- package/dist/vite-plugins/resource-shared.d.ts.map +1 -1
- package/dist/vite-plugins/resource-shared.js +17 -10
- package/dist/vite-plugins/resource-shared.js.map +1 -1
- package/dist/vite-plugins/resources.d.ts +3 -2
- package/dist/vite-plugins/resources.d.ts.map +1 -1
- package/dist/vite-plugins/resources.js +95 -55
- package/dist/vite-plugins/resources.js.map +1 -1
- package/dist/vite-plugins/settings-worker-env.d.ts +2 -1
- package/dist/vite-plugins/settings-worker-env.d.ts.map +1 -1
- package/dist/vite-plugins/settings-worker-env.js +4 -2
- package/dist/vite-plugins/settings-worker-env.js.map +1 -1
- package/dist/vite-plugins/settings.d.ts +2 -1
- package/dist/vite-plugins/settings.d.ts.map +1 -1
- package/dist/vite-plugins/settings.js +56 -32
- package/dist/vite-plugins/settings.js.map +1 -1
- package/dist/vite-plugins/strip-query.d.ts +3 -0
- package/dist/vite-plugins/strip-query.d.ts.map +1 -0
- package/dist/vite-plugins/strip-query.js +6 -0
- package/dist/vite-plugins/strip-query.js.map +1 -0
- package/dist/vite-plugins/undeclared-library.d.ts +3 -1
- package/dist/vite-plugins/undeclared-library.d.ts.map +1 -1
- package/dist/vite-plugins/undeclared-library.js +11 -8
- package/dist/vite-plugins/undeclared-library.js.map +1 -1
- package/dist/vite-plugins/virtual-module.d.ts +2 -2
- package/dist/vite-plugins/virtual-module.d.ts.map +1 -1
- package/dist/vite-plugins/virtual-module.js +5 -3
- package/dist/vite-plugins/virtual-module.js.map +1 -1
- package/dist/vitest-plugin.d.ts.map +1 -1
- package/dist/vitest-plugin.js.map +1 -1
- package/env.d.ts +21 -61
- package/package.json +12 -14
- package/src/babel-modules.d.ts +0 -2
- package/src/builder.ts +7 -6
- package/src/cli.ts +26 -6
- package/src/codegen/freshness.ts +39 -26
- package/src/codegen/report.ts +10 -2
- package/src/codegen/run-codegen.ts +131 -66
- package/src/codegen/store-resolver.ts +52 -15
- package/src/commands/build.ts +3 -2
- package/src/commands/codegen.ts +9 -4
- package/src/commands/dev.ts +3 -4
- package/src/commands/docs.ts +47 -7
- package/src/dev/icon-dir.ts +13 -3
- package/src/dev/refresh-tracker.ts +12 -8
- package/src/dev/runner-deps.ts +30 -7
- package/src/dev/runner-main.ts +4 -3
- package/src/dev/runner.ts +100 -41
- package/src/dev/schema-dir.ts +7 -2
- package/src/dev/supervisor.ts +169 -71
- package/src/dev/vite-dev-server.ts +4 -2
- package/src/internal/banner.ts +5 -1
- package/src/internal/data-dir.ts +47 -16
- package/src/internal/entry-arg.ts +16 -8
- package/src/internal/errors.ts +15 -7
- package/src/internal/list-files.ts +10 -4
- package/src/internal/resolve-cli-tool.ts +28 -12
- package/src/internal/staging-dir.ts +5 -2
- package/src/internal/xdg-data-dirs.ts +8 -2
- package/src/mcp/client.ts +143 -85
- package/src/mcp/handlers.ts +126 -69
- package/src/mcp/index.ts +38 -14
- package/src/mcp/serialize-widget.ts +15 -8
- package/src/mcp/testing-loader.ts +64 -23
- package/src/mcp/widget-registry.ts +11 -5
- package/src/refresh-globals.ts +10 -0
- package/src/refresh-runtime.ts +20 -16
- package/src/settings/compile.ts +7 -4
- package/src/settings/parser.ts +69 -30
- package/src/settings/render.ts +66 -66
- package/src/settings/schema.ts +100 -46
- package/src/vite-plugins/asset-extensions.ts +4 -3
- package/src/vite-plugins/built-url.ts +30 -15
- package/src/vite-plugins/css.ts +35 -20
- package/src/vite-plugins/fast-refresh/refresh-filter.ts +18 -10
- package/src/vite-plugins/fast-refresh/swc-refresh.ts +60 -47
- package/src/vite-plugins/icons.ts +40 -24
- package/src/vite-plugins/index.ts +7 -4
- package/src/vite-plugins/native.ts +14 -5
- package/src/vite-plugins/react-compiler.ts +44 -30
- package/src/vite-plugins/react-dom-prebundle.ts +4 -1
- package/src/vite-plugins/resource-init-module.ts +41 -38
- package/src/vite-plugins/resource-shared.ts +27 -14
- package/src/vite-plugins/resources.ts +154 -60
- package/src/vite-plugins/settings-worker-env.ts +7 -2
- package/src/vite-plugins/settings.ts +107 -42
- package/src/vite-plugins/strip-query.ts +7 -0
- package/src/vite-plugins/undeclared-library.ts +26 -16
- package/src/vite-plugins/virtual-module.ts +9 -2
- 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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
this.
|
|
54
|
-
}
|
|
55
|
-
|
|
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
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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)
|
|
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)
|
|
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
|
|
161
|
+
private async respondToRequest(request: Request): Promise<void> {
|
|
151
162
|
const { id, method, params } = request;
|
|
152
163
|
const connection = this.connection;
|
|
153
|
-
|
|
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 (
|
|
164
|
-
|
|
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 };
|
package/src/mcp/handlers.ts
CHANGED
|
@@ -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 {
|
|
14
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
73
|
-
|
|
160
|
+
}
|
|
161
|
+
case "name": {
|
|
162
|
+
widgets = await matchesOrEmpty(() => testing.findAllByName(app, String(params.value), params.options));
|
|
74
163
|
break;
|
|
75
|
-
|
|
76
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
+
type McpClientController = {
|
|
4
|
+
start: (applicationId: string) => Promise<McpClient>;
|
|
5
|
+
stop: () => void;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const { start: startMcpClient, stop: stopMcpClient } = createMcpClientController();
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
20
|
+
function createMcpClientController(): McpClientController {
|
|
21
|
+
let current: McpClient | null = null;
|
|
11
22
|
|
|
12
|
-
|
|
23
|
+
return {
|
|
24
|
+
start: async (applicationId: string): Promise<McpClient> => {
|
|
25
|
+
if (current) {
|
|
26
|
+
return current;
|
|
27
|
+
}
|
|
13
28
|
|
|
14
|
-
|
|
15
|
-
|
|
29
|
+
const client = new McpClient({ applicationId });
|
|
30
|
+
current = client;
|
|
31
|
+
await connectQuietly(client);
|
|
16
32
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 };
|