@gtkx/mcp 0.11.1 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/eugeniodepalo/gtkx/main/logo.svg" alt="GTKX" width="60" height="60">
3
+ </p>
4
+
5
+ <h1 align="center">GTKX</h1>
6
+
7
+ <p align="center">
8
+ <strong>Build native GTK4 desktop applications with React and TypeScript.</strong>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/@gtkx/react"><img src="https://img.shields.io/npm/v/@gtkx/react.svg" alt="npm version"></a>
13
+ <a href="https://github.com/eugeniodepalo/gtkx/actions"><img src="https://img.shields.io/github/actions/workflow/status/eugeniodepalo/gtkx/ci.yml" alt="CI"></a>
14
+ <a href="https://github.com/eugeniodepalo/gtkx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MPL--2.0-blue.svg" alt="License"></a>
15
+ <a href="https://github.com/eugeniodepalo/gtkx/discussions"><img src="https://img.shields.io/badge/discussions-GitHub-blue" alt="GitHub Discussions"></a>
16
+ </p>
17
+
18
+ ---
19
+
20
+ GTKX lets you write Linux desktop applications using React. Your components render as native GTK4 widgets through a Rust FFI bridge—no webviews, no Electron, just native performance with the developer experience you already know.
21
+
22
+ ## Quick Start
23
+
24
+ ```bash
25
+ npx @gtkx/cli create my-app
26
+ cd my-app
27
+ npm run dev
28
+ ```
29
+
30
+ ## Example
31
+
32
+ ```tsx
33
+ import {
34
+ GtkApplicationWindow,
35
+ GtkBox,
36
+ GtkButton,
37
+ GtkLabel,
38
+ quit,
39
+ render,
40
+ } from "@gtkx/react";
41
+ import * as Gtk from "@gtkx/ffi/gtk";
42
+ import { useState } from "react";
43
+
44
+ const App = () => {
45
+ const [count, setCount] = useState(0);
46
+
47
+ return (
48
+ <GtkApplicationWindow
49
+ title="Counter"
50
+ defaultWidth={300}
51
+ defaultHeight={200}
52
+ onCloseRequest={quit}
53
+ >
54
+ <GtkBox
55
+ orientation={Gtk.Orientation.VERTICAL}
56
+ spacing={20}
57
+ valign={Gtk.Align.CENTER}
58
+ >
59
+ <GtkLabel label={`Count: ${count}`} cssClasses={["title-1"]} />
60
+ <GtkButton label="Increment" onClicked={() => setCount((c) => c + 1)} />
61
+ </GtkBox>
62
+ </GtkApplicationWindow>
63
+ );
64
+ };
65
+
66
+ render(<App />, "com.example.counter");
67
+ ```
68
+
69
+ ## Features
70
+
71
+ - **React 19** — Hooks, concurrent features, and the component model you know
72
+ - **Native GTK4 widgets** — Real native controls, not web components in a webview
73
+ - **Adwaita support** — Modern GNOME styling with Libadwaita components
74
+ - **Hot Module Replacement** — Fast refresh during development
75
+ - **TypeScript first** — Full type safety with auto-generated bindings
76
+ - **CSS-in-JS styling** — Familiar styling patterns adapted for GTK
77
+ - **Testing utilities** — Component testing similar to Testing Library
78
+
79
+ ## Examples
80
+
81
+ Explore complete applications in the [`examples/`](./examples) directory:
82
+
83
+ - **[gtk-demo](./examples/gtk-demo)** — Full replica of the official GTK demo app
84
+ - **[hello-world](./examples/hello-world)** — Minimal application showing a counter
85
+ - **[todo](./examples/todo)** — Full-featured todo application with Adwaita styling and testing
86
+ - **[deploying](./examples/deploying)** — Example of packaging and distributing a GTKX app
87
+
88
+ ## Documentation
89
+
90
+ Visit [https://eugeniodepalo.github.io/gtkx](https://eugeniodepalo.github.io/gtkx/) for the full documentation.
91
+
92
+ ## Contributing
93
+
94
+ Contributions are welcome! Please see the [contributing guidelines](./CONTRIBUTING.md) and check out the [good first issues](https://github.com/eugeniodepalo/gtkx/labels/good%20first%20issue).
95
+
96
+ ## Community
97
+
98
+ - [GitHub Discussions](https://github.com/eugeniodepalo/gtkx/discussions) — Questions, ideas, and general discussion
99
+ - [Issue Tracker](https://github.com/eugeniodepalo/gtkx/issues) — Bug reports and feature requests
100
+
101
+ ## License
102
+
103
+ [MPL-2.0](./LICENSE)
package/dist/cli.js CHANGED
@@ -5,7 +5,6 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
5
5
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
6
6
  import { z } from "zod";
7
7
  import { ConnectionManager } from "./connection-manager.js";
8
- import { noAppConnectedError } from "./protocol/errors.js";
9
8
  import { DEFAULT_SOCKET_PATH } from "./protocol/types.js";
10
9
  import { SocketServer } from "./socket-server.js";
11
10
  const require = createRequire(import.meta.url);
@@ -13,6 +12,13 @@ const { version } = require("../package.json");
13
12
  const AppIdSchema = z.object({
14
13
  appId: z.string().optional().describe("App ID to query. If not specified, uses the first connected app."),
15
14
  });
15
+ const ListAppsInputSchema = z.object({
16
+ waitForApps: z
17
+ .boolean()
18
+ .optional()
19
+ .describe("If true, wait for at least one app to register before returning. Useful when app is still starting."),
20
+ timeout: z.number().optional().describe("Timeout in milliseconds when waitForApps is true (default: 10000)"),
21
+ });
16
22
  const GetWidgetTreeInputSchema = AppIdSchema;
17
23
  const QueryWidgetsInputSchema = AppIdSchema.extend({
18
24
  by: z.enum(["role", "text", "testId", "labelText"]).describe("Query type"),
@@ -41,7 +47,6 @@ const FireEventInputSchema = WidgetIdSchema.extend({
41
47
  });
42
48
  const TakeScreenshotInputSchema = AppIdSchema.extend({
43
49
  windowId: z.string().optional().describe("Window ID to capture. If not specified, captures the first window."),
44
- format: z.enum(["png", "jpeg"]).optional().describe("Image format (default: png)"),
45
50
  });
46
51
  const tools = [
47
52
  {
@@ -49,7 +54,16 @@ const tools = [
49
54
  description: "List all connected GTKX applications",
50
55
  inputSchema: {
51
56
  type: "object",
52
- properties: {},
57
+ properties: {
58
+ waitForApps: {
59
+ type: "boolean",
60
+ description: "If true, wait for at least one app to register before returning. Useful when app is still starting.",
61
+ },
62
+ timeout: {
63
+ type: "number",
64
+ description: "Timeout in milliseconds when waitForApps is true (default: 10000)",
65
+ },
66
+ },
53
67
  required: [],
54
68
  },
55
69
  },
@@ -190,7 +204,7 @@ const tools = [
190
204
  },
191
205
  {
192
206
  name: "gtkx_take_screenshot",
193
- description: "Capture a screenshot of a window. Returns base64-encoded image data.",
207
+ description: "Capture a screenshot of a window. Returns base64-encoded PNG image data.",
194
208
  inputSchema: {
195
209
  type: "object",
196
210
  properties: {
@@ -202,11 +216,6 @@ const tools = [
202
216
  type: "string",
203
217
  description: "Window ID to capture. If not specified, captures the first window.",
204
218
  },
205
- format: {
206
- type: "string",
207
- enum: ["png", "jpeg"],
208
- description: "Image format (default: png)",
209
- },
210
219
  },
211
220
  required: [],
212
221
  },
@@ -215,19 +224,20 @@ const tools = [
215
224
  async function main() {
216
225
  const socketServer = new SocketServer(DEFAULT_SOCKET_PATH);
217
226
  const connectionManager = new ConnectionManager(socketServer);
227
+ socketServer.on("error", (error) => {
228
+ const code = error.code;
229
+ if (code !== "EPIPE" && code !== "ECONNRESET") {
230
+ console.error("[gtkx] Socket error:", error.message);
231
+ }
232
+ });
218
233
  await socketServer.start();
219
- console.error(`[gtkx-mcp] Socket server listening on ${DEFAULT_SOCKET_PATH}`);
234
+ console.error(`[gtkx] Socket server listening on ${DEFAULT_SOCKET_PATH}`);
220
235
  connectionManager.on("appRegistered", (appInfo) => {
221
- console.error(`[gtkx-mcp] App registered: ${appInfo.appId} (PID: ${appInfo.pid})`);
236
+ console.error(`[gtkx] App registered: ${appInfo.appId} (PID: ${appInfo.pid})`);
222
237
  });
223
238
  connectionManager.on("appUnregistered", (appId) => {
224
- console.error(`[gtkx-mcp] App unregistered: ${appId}`);
239
+ console.error(`[gtkx] App unregistered: ${appId}`);
225
240
  });
226
- const requireConnectedApp = () => {
227
- if (!connectionManager.hasConnectedApps()) {
228
- throw noAppConnectedError();
229
- }
230
- };
231
241
  const server = new Server({
232
242
  name: "gtkx-mcp",
233
243
  version,
@@ -244,6 +254,23 @@ async function main() {
244
254
  try {
245
255
  switch (name) {
246
256
  case "gtkx_list_apps": {
257
+ const input = ListAppsInputSchema.parse(args);
258
+ if (input.waitForApps && !connectionManager.hasConnectedApps()) {
259
+ try {
260
+ await connectionManager.waitForApp(input.timeout);
261
+ }
262
+ catch (error) {
263
+ return {
264
+ content: [
265
+ {
266
+ type: "text",
267
+ text: error instanceof Error ? error.message : "Timeout waiting for app",
268
+ },
269
+ ],
270
+ isError: true,
271
+ };
272
+ }
273
+ }
247
274
  const apps = connectionManager.getApps();
248
275
  return {
249
276
  content: [{ type: "text", text: JSON.stringify(apps, null, 2) }],
@@ -251,15 +278,13 @@ async function main() {
251
278
  }
252
279
  case "gtkx_get_widget_tree": {
253
280
  const input = GetWidgetTreeInputSchema.parse(args);
254
- requireConnectedApp();
255
281
  const result = await connectionManager.sendToApp(input.appId, "widget.getTree", {});
256
282
  return {
257
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
283
+ content: [{ type: "text", text: result.tree }],
258
284
  };
259
285
  }
260
286
  case "gtkx_query_widgets": {
261
287
  const input = QueryWidgetsInputSchema.parse(args);
262
- requireConnectedApp();
263
288
  const result = await connectionManager.sendToApp(input.appId, "widget.query", {
264
289
  queryType: input.by,
265
290
  value: input.value,
@@ -271,7 +296,6 @@ async function main() {
271
296
  }
272
297
  case "gtkx_get_widget_props": {
273
298
  const input = GetWidgetPropsInputSchema.parse(args);
274
- requireConnectedApp();
275
299
  const result = await connectionManager.sendToApp(input.appId, "widget.getProps", {
276
300
  widgetId: input.widgetId,
277
301
  });
@@ -281,7 +305,6 @@ async function main() {
281
305
  }
282
306
  case "gtkx_click": {
283
307
  const input = ClickInputSchema.parse(args);
284
- requireConnectedApp();
285
308
  await connectionManager.sendToApp(input.appId, "widget.click", {
286
309
  widgetId: input.widgetId,
287
310
  });
@@ -291,7 +314,6 @@ async function main() {
291
314
  }
292
315
  case "gtkx_type": {
293
316
  const input = TypeInputSchema.parse(args);
294
- requireConnectedApp();
295
317
  await connectionManager.sendToApp(input.appId, "widget.type", {
296
318
  widgetId: input.widgetId,
297
319
  text: input.text,
@@ -303,7 +325,6 @@ async function main() {
303
325
  }
304
326
  case "gtkx_fire_event": {
305
327
  const input = FireEventInputSchema.parse(args);
306
- requireConnectedApp();
307
328
  await connectionManager.sendToApp(input.appId, "widget.fireEvent", {
308
329
  widgetId: input.widgetId,
309
330
  signal: input.signal,
@@ -315,10 +336,8 @@ async function main() {
315
336
  }
316
337
  case "gtkx_take_screenshot": {
317
338
  const input = TakeScreenshotInputSchema.parse(args);
318
- requireConnectedApp();
319
339
  const result = await connectionManager.sendToApp(input.appId, "widget.screenshot", {
320
340
  windowId: input.windowId,
321
- format: input.format,
322
341
  });
323
342
  return {
324
343
  content: [
@@ -365,6 +384,6 @@ async function main() {
365
384
  process.on("SIGTERM", shutdown);
366
385
  }
367
386
  main().catch((error) => {
368
- console.error("[gtkx-mcp] Fatal error:", error);
387
+ console.error("[gtkx] Fatal error:", error);
369
388
  process.exit(1);
370
389
  });
@@ -9,8 +9,14 @@ interface RegisteredApp {
9
9
  info: AppInfo;
10
10
  connection: AppConnection;
11
11
  }
12
+ /**
13
+ * Manages connections between the MCP server and GTKX applications.
14
+ *
15
+ * Handles app registration, request routing, and connection lifecycle.
16
+ */
12
17
  export declare class ConnectionManager extends EventEmitter<ConnectionManagerEventMap> {
13
18
  private socketServer;
19
+ private static readonly DEFAULT_WAIT_TIMEOUT;
14
20
  private apps;
15
21
  private connectionToApp;
16
22
  private pendingRequests;
@@ -22,6 +28,14 @@ export declare class ConnectionManager extends EventEmitter<ConnectionManagerEve
22
28
  getApp(appId: string): AppInfo | undefined;
23
29
  hasConnectedApps(): boolean;
24
30
  getDefaultApp(): RegisteredApp | undefined;
31
+ /**
32
+ * Waits for at least one app to connect and register.
33
+ *
34
+ * @param timeout - Maximum time to wait in milliseconds (default: 10000)
35
+ * @returns Promise that resolves with the first registered app info
36
+ * @throws Error if timeout is reached before any app registers
37
+ */
38
+ waitForApp(timeout?: number): Promise<AppInfo>;
25
39
  sendToApp<T>(appId: string | undefined, method: string, params?: unknown): Promise<T>;
26
40
  cleanup(): void;
27
41
  private handleRequest;
@@ -1,8 +1,14 @@
1
1
  import EventEmitter from "node:events";
2
2
  import { appNotFoundError, invalidRequestError, ipcTimeoutError, McpError, noAppConnectedError, } from "./protocol/errors.js";
3
3
  import { RegisterParamsSchema } from "./protocol/types.js";
4
+ /**
5
+ * Manages connections between the MCP server and GTKX applications.
6
+ *
7
+ * Handles app registration, request routing, and connection lifecycle.
8
+ */
4
9
  export class ConnectionManager extends EventEmitter {
5
10
  socketServer;
11
+ static DEFAULT_WAIT_TIMEOUT = 10000;
6
12
  apps = new Map();
7
13
  connectionToApp = new Map();
8
14
  pendingRequests = new Map();
@@ -14,6 +20,9 @@ export class ConnectionManager extends EventEmitter {
14
20
  this.socketServer.on("request", (connection, request) => {
15
21
  this.handleRequest(connection, request);
16
22
  });
23
+ this.socketServer.on("response", (_connection, response) => {
24
+ this.handleResponse(response);
25
+ });
17
26
  this.socketServer.on("disconnection", (connection) => {
18
27
  this.handleDisconnection(connection);
19
28
  });
@@ -31,6 +40,32 @@ export class ConnectionManager extends EventEmitter {
31
40
  const first = this.apps.values().next();
32
41
  return first.done ? undefined : first.value;
33
42
  }
43
+ /**
44
+ * Waits for at least one app to connect and register.
45
+ *
46
+ * @param timeout - Maximum time to wait in milliseconds (default: 10000)
47
+ * @returns Promise that resolves with the first registered app info
48
+ * @throws Error if timeout is reached before any app registers
49
+ */
50
+ waitForApp(timeout = ConnectionManager.DEFAULT_WAIT_TIMEOUT) {
51
+ const defaultApp = this.getDefaultApp();
52
+ if (defaultApp) {
53
+ return Promise.resolve(defaultApp.info);
54
+ }
55
+ return new Promise((resolve, reject) => {
56
+ const timeoutId = setTimeout(() => {
57
+ this.off("appRegistered", onRegister);
58
+ reject(new Error(`Timeout waiting for app registration after ${timeout}ms. ` +
59
+ "Make sure your GTKX app is running with 'gtkx dev'."));
60
+ }, timeout);
61
+ const onRegister = (appInfo) => {
62
+ clearTimeout(timeoutId);
63
+ this.off("appRegistered", onRegister);
64
+ resolve(appInfo);
65
+ };
66
+ this.on("appRegistered", onRegister);
67
+ });
68
+ }
34
69
  async sendToApp(appId, method, params) {
35
70
  const app = appId ? this.apps.get(appId) : this.getDefaultApp();
36
71
  if (!app) {
@@ -55,7 +90,13 @@ export class ConnectionManager extends EventEmitter {
55
90
  reject,
56
91
  timeout,
57
92
  });
58
- this.socketServer.send(app.connection.id, request);
93
+ const sent = this.socketServer.send(app.connection.id, request);
94
+ if (!sent) {
95
+ clearTimeout(timeout);
96
+ this.pendingRequests.delete(requestId);
97
+ reject(appNotFoundError(app.info.appId));
98
+ return;
99
+ }
59
100
  });
60
101
  }
61
102
  cleanup() {
@@ -74,16 +115,14 @@ export class ConnectionManager extends EventEmitter {
74
115
  this.handleUnregister(connection, request);
75
116
  return;
76
117
  }
77
- this.handleResponse(request);
78
118
  }
79
- handleResponse(message) {
80
- const pending = this.pendingRequests.get(message.id);
119
+ handleResponse(response) {
120
+ const pending = this.pendingRequests.get(response.id);
81
121
  if (!pending) {
82
122
  return;
83
123
  }
84
124
  clearTimeout(pending.timeout);
85
- this.pendingRequests.delete(message.id);
86
- const response = message;
125
+ this.pendingRequests.delete(response.id);
87
126
  if (response.error) {
88
127
  const err = response.error;
89
128
  pending.reject(new McpError(err.code, err.message, err.data));
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { ConnectionManager } from "./connection-manager.js";
2
- export { McpError, McpErrorCode, methodNotFoundError, widgetNotFoundError, } from "./protocol/errors.js";
3
- export { type AppInfo, DEFAULT_SOCKET_PATH, type IpcError, type IpcMethod, type IpcRequest, type IpcResponse, type QueryOptions, type SerializedWidget, } from "./protocol/types.js";
2
+ export { appNotFoundError, invalidRequestError, ipcTimeoutError, McpError, McpErrorCode, methodNotFoundError, noAppConnectedError, widgetNotFoundError, } from "./protocol/errors.js";
3
+ export { type AppInfo, DEFAULT_SOCKET_PATH, getRuntimeDir, type IpcError, IpcErrorSchema, type IpcMessage, type IpcMethod, type IpcRequest, IpcRequestSchema, type IpcResponse, IpcResponseSchema, type QueryOptions, type SerializedWidget, } from "./protocol/types.js";
4
4
  export { type AppConnection, SocketServer } from "./socket-server.js";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { ConnectionManager } from "./connection-manager.js";
2
- export { McpError, McpErrorCode, methodNotFoundError, widgetNotFoundError, } from "./protocol/errors.js";
3
- export { DEFAULT_SOCKET_PATH, } from "./protocol/types.js";
2
+ export { appNotFoundError, invalidRequestError, ipcTimeoutError, McpError, McpErrorCode, methodNotFoundError, noAppConnectedError, widgetNotFoundError, } from "./protocol/errors.js";
3
+ export { DEFAULT_SOCKET_PATH, getRuntimeDir, IpcErrorSchema, IpcRequestSchema, IpcResponseSchema, } from "./protocol/types.js";
4
4
  export { SocketServer } from "./socket-server.js";
@@ -1,30 +1,92 @@
1
+ /**
2
+ * Error codes for MCP protocol errors.
3
+ */
1
4
  export declare enum McpErrorCode {
5
+ /** Internal server error */
2
6
  INTERNAL_ERROR = 1000,
7
+ /** No GTKX application is connected */
3
8
  NO_APP_CONNECTED = 1001,
9
+ /** Requested application ID was not found */
4
10
  APP_NOT_FOUND = 1002,
11
+ /** Widget with specified ID was not found */
5
12
  WIDGET_NOT_FOUND = 1003,
13
+ /** Widget cannot be interacted with */
6
14
  WIDGET_NOT_INTERACTABLE = 1004,
15
+ /** Query timed out waiting for widget */
7
16
  QUERY_TIMEOUT = 1005,
17
+ /** Widget is not the expected type */
8
18
  INVALID_WIDGET_TYPE = 1006,
19
+ /** Screenshot capture failed */
9
20
  SCREENSHOT_FAILED = 1007,
21
+ /** IPC request timed out */
10
22
  IPC_TIMEOUT = 1008,
23
+ /** Failed to serialize data */
11
24
  SERIALIZATION_ERROR = 1009,
25
+ /** Request format is invalid */
12
26
  INVALID_REQUEST = 1010,
27
+ /** Requested method does not exist */
13
28
  METHOD_NOT_FOUND = 1011
14
29
  }
30
+ /**
31
+ * Error class for MCP protocol errors.
32
+ *
33
+ * Contains an error code, message, and optional additional data.
34
+ */
15
35
  export declare class McpError extends Error {
36
+ /** The MCP error code */
16
37
  readonly code: McpErrorCode;
38
+ /** Additional error context */
17
39
  readonly data?: unknown;
18
40
  constructor(code: McpErrorCode, message: string, data?: unknown);
41
+ /**
42
+ * Converts the error to an IPC-compatible format.
43
+ *
44
+ * @returns Object suitable for IPC response error field
45
+ */
19
46
  toIpcError(): {
20
47
  code: number;
21
48
  message: string;
22
49
  data?: unknown;
23
50
  };
24
51
  }
52
+ /**
53
+ * Creates an error for when no GTKX application is connected.
54
+ *
55
+ * @returns McpError with NO_APP_CONNECTED code
56
+ */
25
57
  export declare function noAppConnectedError(): McpError;
58
+ /**
59
+ * Creates an error for when a requested app is not found.
60
+ *
61
+ * @param appId - The application ID that was not found
62
+ * @returns McpError with APP_NOT_FOUND code
63
+ */
26
64
  export declare function appNotFoundError(appId: string): McpError;
65
+ /**
66
+ * Creates an error for when a widget is not found.
67
+ *
68
+ * @param widgetId - The widget ID that was not found
69
+ * @returns McpError with WIDGET_NOT_FOUND code
70
+ */
27
71
  export declare function widgetNotFoundError(widgetId: string): McpError;
72
+ /**
73
+ * Creates an error for when an IPC request times out.
74
+ *
75
+ * @param timeout - The timeout duration in milliseconds
76
+ * @returns McpError with IPC_TIMEOUT code
77
+ */
28
78
  export declare function ipcTimeoutError(timeout: number): McpError;
79
+ /**
80
+ * Creates an error for invalid request format.
81
+ *
82
+ * @param reason - Description of why the request is invalid
83
+ * @returns McpError with INVALID_REQUEST code
84
+ */
29
85
  export declare function invalidRequestError(reason: string): McpError;
86
+ /**
87
+ * Creates an error for when a method is not found.
88
+ *
89
+ * @param method - The method name that was not found
90
+ * @returns McpError with METHOD_NOT_FOUND code
91
+ */
30
92
  export declare function methodNotFoundError(method: string): McpError;
@@ -1,20 +1,42 @@
1
+ /**
2
+ * Error codes for MCP protocol errors.
3
+ */
1
4
  export var McpErrorCode;
2
5
  (function (McpErrorCode) {
6
+ /** Internal server error */
3
7
  McpErrorCode[McpErrorCode["INTERNAL_ERROR"] = 1000] = "INTERNAL_ERROR";
8
+ /** No GTKX application is connected */
4
9
  McpErrorCode[McpErrorCode["NO_APP_CONNECTED"] = 1001] = "NO_APP_CONNECTED";
10
+ /** Requested application ID was not found */
5
11
  McpErrorCode[McpErrorCode["APP_NOT_FOUND"] = 1002] = "APP_NOT_FOUND";
12
+ /** Widget with specified ID was not found */
6
13
  McpErrorCode[McpErrorCode["WIDGET_NOT_FOUND"] = 1003] = "WIDGET_NOT_FOUND";
14
+ /** Widget cannot be interacted with */
7
15
  McpErrorCode[McpErrorCode["WIDGET_NOT_INTERACTABLE"] = 1004] = "WIDGET_NOT_INTERACTABLE";
16
+ /** Query timed out waiting for widget */
8
17
  McpErrorCode[McpErrorCode["QUERY_TIMEOUT"] = 1005] = "QUERY_TIMEOUT";
18
+ /** Widget is not the expected type */
9
19
  McpErrorCode[McpErrorCode["INVALID_WIDGET_TYPE"] = 1006] = "INVALID_WIDGET_TYPE";
20
+ /** Screenshot capture failed */
10
21
  McpErrorCode[McpErrorCode["SCREENSHOT_FAILED"] = 1007] = "SCREENSHOT_FAILED";
22
+ /** IPC request timed out */
11
23
  McpErrorCode[McpErrorCode["IPC_TIMEOUT"] = 1008] = "IPC_TIMEOUT";
24
+ /** Failed to serialize data */
12
25
  McpErrorCode[McpErrorCode["SERIALIZATION_ERROR"] = 1009] = "SERIALIZATION_ERROR";
26
+ /** Request format is invalid */
13
27
  McpErrorCode[McpErrorCode["INVALID_REQUEST"] = 1010] = "INVALID_REQUEST";
28
+ /** Requested method does not exist */
14
29
  McpErrorCode[McpErrorCode["METHOD_NOT_FOUND"] = 1011] = "METHOD_NOT_FOUND";
15
30
  })(McpErrorCode || (McpErrorCode = {}));
31
+ /**
32
+ * Error class for MCP protocol errors.
33
+ *
34
+ * Contains an error code, message, and optional additional data.
35
+ */
16
36
  export class McpError extends Error {
37
+ /** The MCP error code */
17
38
  code;
39
+ /** Additional error context */
18
40
  data;
19
41
  constructor(code, message, data) {
20
42
  super(message);
@@ -25,6 +47,11 @@ export class McpError extends Error {
25
47
  Error.captureStackTrace(this, McpError);
26
48
  }
27
49
  }
50
+ /**
51
+ * Converts the error to an IPC-compatible format.
52
+ *
53
+ * @returns Object suitable for IPC response error field
54
+ */
28
55
  toIpcError() {
29
56
  return {
30
57
  code: this.code,
@@ -33,21 +60,56 @@ export class McpError extends Error {
33
60
  };
34
61
  }
35
62
  }
63
+ /**
64
+ * Creates an error for when no GTKX application is connected.
65
+ *
66
+ * @returns McpError with NO_APP_CONNECTED code
67
+ */
36
68
  export function noAppConnectedError() {
37
69
  return new McpError(McpErrorCode.NO_APP_CONNECTED, "No GTKX application connected. Start an app with 'gtkx dev' to connect.", { hint: "Run 'gtkx dev src/app.tsx' in your project directory" });
38
70
  }
71
+ /**
72
+ * Creates an error for when a requested app is not found.
73
+ *
74
+ * @param appId - The application ID that was not found
75
+ * @returns McpError with APP_NOT_FOUND code
76
+ */
39
77
  export function appNotFoundError(appId) {
40
78
  return new McpError(McpErrorCode.APP_NOT_FOUND, `Application '${appId}' not found`, { appId });
41
79
  }
80
+ /**
81
+ * Creates an error for when a widget is not found.
82
+ *
83
+ * @param widgetId - The widget ID that was not found
84
+ * @returns McpError with WIDGET_NOT_FOUND code
85
+ */
42
86
  export function widgetNotFoundError(widgetId) {
43
87
  return new McpError(McpErrorCode.WIDGET_NOT_FOUND, `Widget '${widgetId}' not found`, { widgetId });
44
88
  }
89
+ /**
90
+ * Creates an error for when an IPC request times out.
91
+ *
92
+ * @param timeout - The timeout duration in milliseconds
93
+ * @returns McpError with IPC_TIMEOUT code
94
+ */
45
95
  export function ipcTimeoutError(timeout) {
46
96
  return new McpError(McpErrorCode.IPC_TIMEOUT, `IPC request timed out after ${timeout}ms`, { timeout });
47
97
  }
98
+ /**
99
+ * Creates an error for invalid request format.
100
+ *
101
+ * @param reason - Description of why the request is invalid
102
+ * @returns McpError with INVALID_REQUEST code
103
+ */
48
104
  export function invalidRequestError(reason) {
49
105
  return new McpError(McpErrorCode.INVALID_REQUEST, `Invalid request: ${reason}`, { reason });
50
106
  }
107
+ /**
108
+ * Creates an error for when a method is not found.
109
+ *
110
+ * @param method - The method name that was not found
111
+ * @returns McpError with METHOD_NOT_FOUND code
112
+ */
51
113
  export function methodNotFoundError(method) {
52
114
  return new McpError(McpErrorCode.METHOD_NOT_FOUND, `Method '${method}' not found`, { method });
53
115
  }
@@ -1,39 +1,76 @@
1
1
  import { z } from "zod";
2
+ /**
3
+ * Zod schema for validating IPC requests.
4
+ */
2
5
  export declare const IpcRequestSchema: z.ZodObject<{
3
6
  id: z.ZodString;
4
7
  method: z.ZodString;
5
8
  params: z.ZodOptional<z.ZodUnknown>;
6
- }, "strip", z.ZodTypeAny, {
7
- method: string;
8
- id: string;
9
- params?: unknown;
10
- }, {
11
- method: string;
12
- id: string;
13
- params?: unknown;
14
- }>;
9
+ }, z.core.$strip>;
10
+ /**
11
+ * An IPC request message.
12
+ */
15
13
  export type IpcRequest = z.infer<typeof IpcRequestSchema>;
14
+ /**
15
+ * An IPC error object.
16
+ */
16
17
  export type IpcError = {
18
+ /** Error code */
17
19
  code: number;
20
+ /** Error message */
18
21
  message: string;
22
+ /** Additional error data */
19
23
  data?: unknown;
20
24
  };
21
- export type IpcResponse = {
22
- id: string;
23
- result?: unknown;
24
- error?: IpcError;
25
- };
25
+ /**
26
+ * Zod schema for validating IPC errors.
27
+ */
28
+ export declare const IpcErrorSchema: z.ZodObject<{
29
+ code: z.ZodNumber;
30
+ message: z.ZodString;
31
+ data: z.ZodOptional<z.ZodUnknown>;
32
+ }, z.core.$strip>;
33
+ /**
34
+ * Zod schema for validating IPC responses.
35
+ */
36
+ export declare const IpcResponseSchema: z.ZodObject<{
37
+ id: z.ZodString;
38
+ result: z.ZodOptional<z.ZodUnknown>;
39
+ error: z.ZodOptional<z.ZodObject<{
40
+ code: z.ZodNumber;
41
+ message: z.ZodString;
42
+ data: z.ZodOptional<z.ZodUnknown>;
43
+ }, z.core.$strip>>;
44
+ }, z.core.$strip>;
45
+ /**
46
+ * An IPC response message.
47
+ */
48
+ export type IpcResponse = z.infer<typeof IpcResponseSchema>;
49
+ /**
50
+ * A serialized representation of a GTK widget for IPC transfer.
51
+ */
26
52
  export interface SerializedWidget {
53
+ /** Unique widget identifier */
27
54
  id: string;
55
+ /** Widget type name (e.g., "GtkButton") */
28
56
  type: string;
57
+ /** Accessible role */
29
58
  role: string;
59
+ /** Widget name (test ID) */
30
60
  name: string | null;
61
+ /** Accessible label */
31
62
  label: string | null;
63
+ /** Text content */
32
64
  text: string | null;
65
+ /** Whether the widget is sensitive (interactive) */
33
66
  sensitive: boolean;
67
+ /** Whether the widget is visible */
34
68
  visible: boolean;
69
+ /** CSS class names */
35
70
  cssClasses: string[];
71
+ /** Child widgets */
36
72
  children: SerializedWidget[];
73
+ /** Widget bounds in window coordinates */
37
74
  bounds?: {
38
75
  x: number;
39
76
  y: number;
@@ -41,31 +78,54 @@ export interface SerializedWidget {
41
78
  height: number;
42
79
  };
43
80
  }
81
+ /**
82
+ * Information about a connected GTKX application.
83
+ */
44
84
  export type AppInfo = {
85
+ /** Application ID (e.g., "com.example.myapp") */
45
86
  appId: string;
87
+ /** Process ID */
46
88
  pid: number;
89
+ /** Open windows */
47
90
  windows: Array<{
48
91
  id: string;
49
92
  title: string | null;
50
93
  }>;
51
94
  };
95
+ /**
96
+ * Options for widget queries.
97
+ */
52
98
  export type QueryOptions = {
99
+ /** Widget name to match */
53
100
  name?: string;
54
- checked?: boolean;
55
- expanded?: boolean;
101
+ /** Require exact match */
56
102
  exact?: boolean;
103
+ /** Query timeout in milliseconds */
57
104
  timeout?: number;
58
105
  };
106
+ /**
107
+ * Zod schema for app registration parameters.
108
+ * @internal
109
+ */
59
110
  export declare const RegisterParamsSchema: z.ZodObject<{
60
111
  appId: z.ZodString;
61
112
  pid: z.ZodNumber;
62
- }, "strip", z.ZodTypeAny, {
63
- appId: string;
64
- pid: number;
65
- }, {
66
- appId: string;
67
- pid: number;
68
- }>;
113
+ }, z.core.$strip>;
114
+ /**
115
+ * Available IPC methods.
116
+ */
69
117
  export type IpcMethod = "app.register" | "app.unregister" | "widget.getTree" | "widget.query" | "widget.getProps" | "widget.click" | "widget.type" | "widget.fireEvent" | "widget.screenshot";
118
+ /**
119
+ * Union type for any IPC message (request or response).
120
+ */
70
121
  export type IpcMessage = IpcRequest | IpcResponse;
71
- export declare const DEFAULT_SOCKET_PATH = "/tmp/gtkx-mcp.sock";
122
+ /**
123
+ * Gets the XDG runtime directory or falls back to system temp.
124
+ *
125
+ * @returns Path to the runtime directory
126
+ */
127
+ export declare const getRuntimeDir: () => string;
128
+ /**
129
+ * Default path for the MCP socket file.
130
+ */
131
+ export declare const DEFAULT_SOCKET_PATH: string;
@@ -1,11 +1,45 @@
1
+ import { tmpdir } from "node:os";
2
+ import { join } from "node:path";
1
3
  import { z } from "zod";
4
+ /**
5
+ * Zod schema for validating IPC requests.
6
+ */
2
7
  export const IpcRequestSchema = z.object({
3
8
  id: z.string(),
4
9
  method: z.string(),
5
10
  params: z.unknown().optional(),
6
11
  });
12
+ /**
13
+ * Zod schema for validating IPC errors.
14
+ */
15
+ export const IpcErrorSchema = z.object({
16
+ code: z.number(),
17
+ message: z.string(),
18
+ data: z.unknown().optional(),
19
+ });
20
+ /**
21
+ * Zod schema for validating IPC responses.
22
+ */
23
+ export const IpcResponseSchema = z.object({
24
+ id: z.string(),
25
+ result: z.unknown().optional(),
26
+ error: IpcErrorSchema.optional(),
27
+ });
28
+ /**
29
+ * Zod schema for app registration parameters.
30
+ * @internal
31
+ */
7
32
  export const RegisterParamsSchema = z.object({
8
33
  appId: z.string(),
9
34
  pid: z.number(),
10
35
  });
11
- export const DEFAULT_SOCKET_PATH = "/tmp/gtkx-mcp.sock";
36
+ /**
37
+ * Gets the XDG runtime directory or falls back to system temp.
38
+ *
39
+ * @returns Path to the runtime directory
40
+ */
41
+ export const getRuntimeDir = () => process.env.XDG_RUNTIME_DIR ?? tmpdir();
42
+ /**
43
+ * Default path for the MCP socket file.
44
+ */
45
+ export const DEFAULT_SOCKET_PATH = join(getRuntimeDir(), "gtkx-mcp.sock");
@@ -1,17 +1,29 @@
1
1
  import EventEmitter from "node:events";
2
2
  import * as net from "node:net";
3
- import { type IpcMessage, type IpcRequest } from "./protocol/types.js";
3
+ import { type IpcMessage, type IpcRequest, type IpcResponse } from "./protocol/types.js";
4
4
  type SocketServerEventMap = {
5
5
  connection: [AppConnection];
6
6
  disconnection: [AppConnection];
7
7
  request: [AppConnection, IpcRequest];
8
+ response: [AppConnection, IpcResponse];
8
9
  error: [Error];
9
10
  };
11
+ /**
12
+ * Represents a connected application.
13
+ */
10
14
  export interface AppConnection {
15
+ /** Unique connection identifier */
11
16
  id: string;
17
+ /** The underlying socket */
12
18
  socket: net.Socket;
19
+ /** Buffer for incomplete messages */
13
20
  buffer: string;
14
21
  }
22
+ /**
23
+ * Unix domain socket server for MCP communication.
24
+ *
25
+ * Manages connections from GTKX applications and handles IPC messaging.
26
+ */
15
27
  export declare class SocketServer extends EventEmitter<SocketServerEventMap> {
16
28
  private server;
17
29
  private connections;
@@ -2,7 +2,12 @@ import EventEmitter from "node:events";
2
2
  import * as fs from "node:fs";
3
3
  import * as net from "node:net";
4
4
  import { invalidRequestError } from "./protocol/errors.js";
5
- import { DEFAULT_SOCKET_PATH, IpcRequestSchema, } from "./protocol/types.js";
5
+ import { DEFAULT_SOCKET_PATH, IpcRequestSchema, IpcResponseSchema, } from "./protocol/types.js";
6
+ /**
7
+ * Unix domain socket server for MCP communication.
8
+ *
9
+ * Manages connections from GTKX applications and handles IPC messaging.
10
+ */
6
11
  export class SocketServer extends EventEmitter {
7
12
  server = null;
8
13
  connections = new Map();
@@ -61,7 +66,7 @@ export class SocketServer extends EventEmitter {
61
66
  }
62
67
  send(connectionId, message) {
63
68
  const connection = this.connections.get(connectionId);
64
- if (!connection) {
69
+ if (!connection || !connection.socket.writable) {
65
70
  return false;
66
71
  }
67
72
  const data = `${JSON.stringify(message)}\n`;
@@ -111,15 +116,26 @@ export class SocketServer extends EventEmitter {
111
116
  this.send(connection.id, response);
112
117
  return;
113
118
  }
114
- const result = IpcRequestSchema.safeParse(parsed);
115
- if (!result.success) {
116
- const response = {
117
- id: parsed.id ?? "unknown",
118
- error: invalidRequestError(result.error.message).toIpcError(),
119
- };
120
- this.send(connection.id, response);
121
- return;
119
+ const message = parsed;
120
+ const hasMethod = typeof message.method === "string";
121
+ if (hasMethod) {
122
+ const requestResult = IpcRequestSchema.safeParse(parsed);
123
+ if (requestResult.success) {
124
+ this.emit("request", connection, requestResult.data);
125
+ return;
126
+ }
127
+ }
128
+ else {
129
+ const responseResult = IpcResponseSchema.safeParse(parsed);
130
+ if (responseResult.success) {
131
+ this.emit("response", connection, responseResult.data);
132
+ return;
133
+ }
122
134
  }
123
- this.emit("request", connection, result.data);
135
+ const response = {
136
+ id: message.id ?? "unknown",
137
+ error: invalidRequestError("Invalid message format").toIpcError(),
138
+ };
139
+ this.send(connection.id, response);
124
140
  }
125
141
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtkx/mcp",
3
- "version": "0.11.1",
3
+ "version": "0.12.1",
4
4
  "description": "MCP server for AI-powered interaction with GTKX applications",
5
5
  "keywords": [
6
6
  "gtkx",
@@ -40,12 +40,11 @@
40
40
  "dist"
41
41
  ],
42
42
  "dependencies": {
43
- "@modelcontextprotocol/sdk": "^1.0.0",
44
- "zod": "^3.24.0"
43
+ "@modelcontextprotocol/sdk": "^1.25.1",
44
+ "zod": "^4.3.5"
45
45
  },
46
- "devDependencies": {},
47
46
  "scripts": {
48
- "build": "tsc -b",
47
+ "build": "tsc -b && cp ../../README.md .",
49
48
  "test": "vitest run"
50
49
  }
51
50
  }