@gtkx/mcp 0.21.0 → 1.0.0-rc.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 +137 -34
- package/bin/gtkx-mcp.js +8 -1
- package/dist/app-router.d.ts +35 -0
- package/dist/app-router.d.ts.map +1 -0
- package/dist/app-router.js +130 -0
- package/dist/app-router.js.map +1 -0
- package/dist/connection-registry.d.ts +12 -0
- package/dist/connection-registry.d.ts.map +1 -0
- package/dist/connection-registry.js +38 -0
- package/dist/connection-registry.js.map +1 -0
- package/dist/internal.d.ts +4 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +4 -0
- package/dist/internal.js.map +1 -0
- package/dist/protocol/errors.d.ts +24 -87
- package/dist/protocol/errors.d.ts.map +1 -1
- package/dist/protocol/errors.js +27 -91
- package/dist/protocol/errors.js.map +1 -1
- package/dist/protocol/schemas.d.ts +89 -0
- package/dist/protocol/schemas.d.ts.map +1 -0
- package/dist/protocol/schemas.js +56 -0
- package/dist/protocol/schemas.js.map +1 -0
- package/dist/reference.d.ts +13 -0
- package/dist/reference.d.ts.map +1 -0
- package/dist/reference.js +250 -0
- package/dist/reference.js.map +1 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +220 -0
- package/dist/server.js.map +1 -0
- package/dist/socket-server.d.ts +4 -38
- package/dist/socket-server.d.ts.map +1 -1
- package/dist/socket-server.js +26 -109
- package/dist/socket-server.js.map +1 -1
- package/dist/tool.d.ts +22 -0
- package/dist/tool.d.ts.map +1 -0
- package/dist/tool.js +36 -0
- package/dist/tool.js.map +1 -0
- package/dist/transport.d.ts +41 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +121 -0
- package/dist/transport.js.map +1 -0
- package/package.json +17 -11
- package/src/app-router.ts +172 -0
- package/src/connection-registry.ts +42 -0
- package/src/internal.ts +17 -0
- package/src/protocol/errors.ts +43 -99
- package/src/protocol/schemas.ts +148 -0
- package/src/reference.ts +340 -0
- package/src/server.ts +281 -0
- package/src/socket-server.ts +30 -154
- package/src/tool.ts +66 -0
- package/src/transport.ts +165 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -399
- package/dist/cli.js.map +0 -1
- package/dist/connection-manager.d.ts +0 -48
- package/dist/connection-manager.d.ts.map +0 -1
- package/dist/connection-manager.js +0 -185
- package/dist/connection-manager.js.map +0 -1
- package/dist/index.d.ts +0 -5
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -5
- package/dist/index.js.map +0 -1
- package/dist/protocol/types.d.ts +0 -132
- package/dist/protocol/types.d.ts.map +0 -1
- package/dist/protocol/types.js +0 -46
- package/dist/protocol/types.js.map +0 -1
- package/src/cli.ts +0 -449
- package/src/connection-manager.ts +0 -247
- package/src/index.ts +0 -27
- package/src/protocol/types.ts +0 -153
package/src/cli.ts
DELETED
|
@@ -1,449 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
-
import { z } from "zod";
|
|
8
|
-
import { ConnectionManager } from "./connection-manager.js";
|
|
9
|
-
import { DEFAULT_SOCKET_PATH } from "./protocol/types.js";
|
|
10
|
-
import { SocketServer } from "./socket-server.js";
|
|
11
|
-
|
|
12
|
-
const require = createRequire(import.meta.url);
|
|
13
|
-
const { version } = require("../package.json") as { version: string };
|
|
14
|
-
|
|
15
|
-
const AppIdSchema = z.object({
|
|
16
|
-
appId: z.string().optional().describe("App ID to query. If not specified, uses the first connected app."),
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
const ListAppsInputSchema = z.object({
|
|
20
|
-
waitForApps: z
|
|
21
|
-
.boolean()
|
|
22
|
-
.optional()
|
|
23
|
-
.describe(
|
|
24
|
-
"If true, wait for at least one app to register before returning. Useful when app is still starting.",
|
|
25
|
-
),
|
|
26
|
-
timeout: z.number().optional().describe("Timeout in milliseconds when waitForApps is true (default: 10000)"),
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
const GetWidgetTreeInputSchema = AppIdSchema;
|
|
30
|
-
|
|
31
|
-
const QueryWidgetsInputSchema = AppIdSchema.extend({
|
|
32
|
-
by: z.enum(["role", "text", "name", "labelText"]).describe("Query type"),
|
|
33
|
-
value: z.union([z.string(), z.number()]).describe("Value to search for"),
|
|
34
|
-
options: z
|
|
35
|
-
.object({
|
|
36
|
-
name: z.string().optional(),
|
|
37
|
-
exact: z.boolean().optional(),
|
|
38
|
-
timeout: z.number().optional(),
|
|
39
|
-
})
|
|
40
|
-
.optional()
|
|
41
|
-
.describe("Additional query options"),
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const WidgetIdSchema = AppIdSchema.extend({
|
|
45
|
-
widgetId: z.string().describe("Widget ID"),
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const GetWidgetPropsInputSchema = WidgetIdSchema;
|
|
49
|
-
|
|
50
|
-
const ClickInputSchema = WidgetIdSchema;
|
|
51
|
-
|
|
52
|
-
const TypeInputSchema = WidgetIdSchema.extend({
|
|
53
|
-
text: z.string().describe("Text to type"),
|
|
54
|
-
clear: z.boolean().optional().describe("Clear existing text before typing"),
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const FireEventInputSchema = WidgetIdSchema.extend({
|
|
58
|
-
signal: z.string().describe("GTK signal name to emit"),
|
|
59
|
-
args: z.array(z.unknown()).optional().describe("Arguments to pass to the signal"),
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const TakeScreenshotInputSchema = AppIdSchema.extend({
|
|
63
|
-
windowId: z.string().optional().describe("Window ID to capture. If not specified, captures the first window."),
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
const tools = [
|
|
67
|
-
{
|
|
68
|
-
name: "gtkx_list_apps",
|
|
69
|
-
description: "List all connected GTKX applications",
|
|
70
|
-
inputSchema: {
|
|
71
|
-
type: "object" as const,
|
|
72
|
-
properties: {
|
|
73
|
-
waitForApps: {
|
|
74
|
-
type: "boolean",
|
|
75
|
-
description:
|
|
76
|
-
"If true, wait for at least one app to register before returning. Useful when app is still starting.",
|
|
77
|
-
},
|
|
78
|
-
timeout: {
|
|
79
|
-
type: "number",
|
|
80
|
-
description: "Timeout in milliseconds when waitForApps is true (default: 10000)",
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
required: [],
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
name: "gtkx_get_widget_tree",
|
|
88
|
-
description:
|
|
89
|
-
"Get the widget hierarchy for a connected GTKX app. Returns a tree of all widgets with their IDs, types, roles, and properties.",
|
|
90
|
-
inputSchema: {
|
|
91
|
-
type: "object" as const,
|
|
92
|
-
properties: {
|
|
93
|
-
appId: {
|
|
94
|
-
type: "string",
|
|
95
|
-
description: "App ID to query. If not specified, uses the first connected app.",
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
required: [],
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
name: "gtkx_query_widgets",
|
|
103
|
-
description:
|
|
104
|
-
"Find widgets by role, text, name, or label. Returns matching widgets with their IDs and properties.",
|
|
105
|
-
inputSchema: {
|
|
106
|
-
type: "object" as const,
|
|
107
|
-
properties: {
|
|
108
|
-
appId: {
|
|
109
|
-
type: "string",
|
|
110
|
-
description: "App ID to query. If not specified, uses the first connected app.",
|
|
111
|
-
},
|
|
112
|
-
by: {
|
|
113
|
-
type: "string",
|
|
114
|
-
enum: ["role", "text", "name", "labelText"],
|
|
115
|
-
description: "Query type",
|
|
116
|
-
},
|
|
117
|
-
value: {
|
|
118
|
-
oneOf: [{ type: "string" }, { type: "number" }],
|
|
119
|
-
description: "Value to search for",
|
|
120
|
-
},
|
|
121
|
-
options: {
|
|
122
|
-
type: "object",
|
|
123
|
-
properties: {
|
|
124
|
-
name: { type: "string" },
|
|
125
|
-
exact: { type: "boolean" },
|
|
126
|
-
timeout: { type: "number" },
|
|
127
|
-
},
|
|
128
|
-
description: "Additional query options",
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
required: ["by", "value"],
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
name: "gtkx_get_widget_props",
|
|
136
|
-
description: "Get all properties of a specific widget by its ID",
|
|
137
|
-
inputSchema: {
|
|
138
|
-
type: "object" as const,
|
|
139
|
-
properties: {
|
|
140
|
-
appId: {
|
|
141
|
-
type: "string",
|
|
142
|
-
description: "App ID to query. If not specified, uses the first connected app.",
|
|
143
|
-
},
|
|
144
|
-
widgetId: {
|
|
145
|
-
type: "string",
|
|
146
|
-
description: "Widget ID to get properties for",
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
required: ["widgetId"],
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
name: "gtkx_click",
|
|
154
|
-
description: "Click a widget. Works with buttons, checkboxes, and other interactive widgets.",
|
|
155
|
-
inputSchema: {
|
|
156
|
-
type: "object" as const,
|
|
157
|
-
properties: {
|
|
158
|
-
appId: {
|
|
159
|
-
type: "string",
|
|
160
|
-
description: "App ID to query. If not specified, uses the first connected app.",
|
|
161
|
-
},
|
|
162
|
-
widgetId: {
|
|
163
|
-
type: "string",
|
|
164
|
-
description: "Widget ID to click",
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
required: ["widgetId"],
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
name: "gtkx_type",
|
|
172
|
-
description: "Type text into an editable widget like Entry or TextView",
|
|
173
|
-
inputSchema: {
|
|
174
|
-
type: "object" as const,
|
|
175
|
-
properties: {
|
|
176
|
-
appId: {
|
|
177
|
-
type: "string",
|
|
178
|
-
description: "App ID to query. If not specified, uses the first connected app.",
|
|
179
|
-
},
|
|
180
|
-
widgetId: {
|
|
181
|
-
type: "string",
|
|
182
|
-
description: "Widget ID to type into",
|
|
183
|
-
},
|
|
184
|
-
text: {
|
|
185
|
-
type: "string",
|
|
186
|
-
description: "Text to type",
|
|
187
|
-
},
|
|
188
|
-
clear: {
|
|
189
|
-
type: "boolean",
|
|
190
|
-
description: "Clear existing text before typing",
|
|
191
|
-
},
|
|
192
|
-
},
|
|
193
|
-
required: ["widgetId", "text"],
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
name: "gtkx_fire_event",
|
|
198
|
-
description: "Emit a GTK signal on a widget. Use this for custom interactions.",
|
|
199
|
-
inputSchema: {
|
|
200
|
-
type: "object" as const,
|
|
201
|
-
properties: {
|
|
202
|
-
appId: {
|
|
203
|
-
type: "string",
|
|
204
|
-
description: "App ID to query. If not specified, uses the first connected app.",
|
|
205
|
-
},
|
|
206
|
-
widgetId: {
|
|
207
|
-
type: "string",
|
|
208
|
-
description: "Widget ID to emit event on",
|
|
209
|
-
},
|
|
210
|
-
signal: {
|
|
211
|
-
type: "string",
|
|
212
|
-
description: "GTK signal name to emit",
|
|
213
|
-
},
|
|
214
|
-
args: {
|
|
215
|
-
type: "array",
|
|
216
|
-
items: {},
|
|
217
|
-
description: "Arguments to pass to the signal",
|
|
218
|
-
},
|
|
219
|
-
},
|
|
220
|
-
required: ["widgetId", "signal"],
|
|
221
|
-
},
|
|
222
|
-
},
|
|
223
|
-
{
|
|
224
|
-
name: "gtkx_take_screenshot",
|
|
225
|
-
description: "Capture a screenshot of a window. Returns base64-encoded PNG image data.",
|
|
226
|
-
inputSchema: {
|
|
227
|
-
type: "object" as const,
|
|
228
|
-
properties: {
|
|
229
|
-
appId: {
|
|
230
|
-
type: "string",
|
|
231
|
-
description: "App ID to query. If not specified, uses the first connected app.",
|
|
232
|
-
},
|
|
233
|
-
windowId: {
|
|
234
|
-
type: "string",
|
|
235
|
-
description: "Window ID to capture. If not specified, captures the first window.",
|
|
236
|
-
},
|
|
237
|
-
},
|
|
238
|
-
required: [],
|
|
239
|
-
},
|
|
240
|
-
},
|
|
241
|
-
];
|
|
242
|
-
|
|
243
|
-
async function main() {
|
|
244
|
-
const socketServer = new SocketServer(DEFAULT_SOCKET_PATH);
|
|
245
|
-
const connectionManager = new ConnectionManager(socketServer);
|
|
246
|
-
|
|
247
|
-
socketServer.on("error", (error) => {
|
|
248
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
249
|
-
if (code !== "EPIPE" && code !== "ECONNRESET") {
|
|
250
|
-
console.error("[gtkx] Socket error:", error.message);
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
await socketServer.start();
|
|
255
|
-
console.error(`[gtkx] Socket server listening on ${DEFAULT_SOCKET_PATH}`);
|
|
256
|
-
|
|
257
|
-
connectionManager.on("appRegistered", (appInfo) => {
|
|
258
|
-
console.error(`[gtkx] App registered: ${appInfo.appId} (PID: ${appInfo.pid})`);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
connectionManager.on("appUnregistered", (appId) => {
|
|
262
|
-
console.error(`[gtkx] App unregistered: ${appId}`);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
const server = new Server(
|
|
266
|
-
{
|
|
267
|
-
name: "gtkx-mcp",
|
|
268
|
-
version,
|
|
269
|
-
},
|
|
270
|
-
{
|
|
271
|
-
capabilities: {
|
|
272
|
-
tools: {},
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
278
|
-
return { tools };
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
282
|
-
const { name, arguments: args } = request.params;
|
|
283
|
-
|
|
284
|
-
try {
|
|
285
|
-
switch (name) {
|
|
286
|
-
case "gtkx_list_apps": {
|
|
287
|
-
const input = ListAppsInputSchema.parse(args);
|
|
288
|
-
|
|
289
|
-
if (input.waitForApps && !connectionManager.hasConnectedApps()) {
|
|
290
|
-
try {
|
|
291
|
-
await connectionManager.waitForApp(input.timeout);
|
|
292
|
-
} catch (error) {
|
|
293
|
-
return {
|
|
294
|
-
content: [
|
|
295
|
-
{
|
|
296
|
-
type: "text",
|
|
297
|
-
text: error instanceof Error ? error.message : "Timeout waiting for app",
|
|
298
|
-
},
|
|
299
|
-
],
|
|
300
|
-
isError: true,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const apps = connectionManager.getApps();
|
|
306
|
-
const appsWithWindows = await Promise.all(
|
|
307
|
-
apps.map(async (app) => {
|
|
308
|
-
try {
|
|
309
|
-
const result = await connectionManager.sendToApp<{
|
|
310
|
-
windows: Array<{ id: string; title: string | null }>;
|
|
311
|
-
}>(app.appId, "app.getWindows", {});
|
|
312
|
-
return { ...app, windows: result.windows };
|
|
313
|
-
} catch {
|
|
314
|
-
return app;
|
|
315
|
-
}
|
|
316
|
-
}),
|
|
317
|
-
);
|
|
318
|
-
return {
|
|
319
|
-
content: [{ type: "text", text: JSON.stringify(appsWithWindows, null, 2) }],
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
case "gtkx_get_widget_tree": {
|
|
324
|
-
const input = GetWidgetTreeInputSchema.parse(args);
|
|
325
|
-
const result = await connectionManager.sendToApp<{ tree: string }>(
|
|
326
|
-
input.appId,
|
|
327
|
-
"widget.getTree",
|
|
328
|
-
{},
|
|
329
|
-
);
|
|
330
|
-
return {
|
|
331
|
-
content: [{ type: "text", text: result.tree }],
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
case "gtkx_query_widgets": {
|
|
336
|
-
const input = QueryWidgetsInputSchema.parse(args);
|
|
337
|
-
const result = await connectionManager.sendToApp(input.appId, "widget.query", {
|
|
338
|
-
queryType: input.by,
|
|
339
|
-
value: input.value,
|
|
340
|
-
options: input.options,
|
|
341
|
-
});
|
|
342
|
-
return {
|
|
343
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
case "gtkx_get_widget_props": {
|
|
348
|
-
const input = GetWidgetPropsInputSchema.parse(args);
|
|
349
|
-
const result = await connectionManager.sendToApp(input.appId, "widget.getProps", {
|
|
350
|
-
widgetId: input.widgetId,
|
|
351
|
-
});
|
|
352
|
-
return {
|
|
353
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
case "gtkx_click": {
|
|
358
|
-
const input = ClickInputSchema.parse(args);
|
|
359
|
-
await connectionManager.sendToApp(input.appId, "widget.click", {
|
|
360
|
-
widgetId: input.widgetId,
|
|
361
|
-
});
|
|
362
|
-
return {
|
|
363
|
-
content: [{ type: "text", text: "Click successful" }],
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
case "gtkx_type": {
|
|
368
|
-
const input = TypeInputSchema.parse(args);
|
|
369
|
-
await connectionManager.sendToApp(input.appId, "widget.type", {
|
|
370
|
-
widgetId: input.widgetId,
|
|
371
|
-
text: input.text,
|
|
372
|
-
clear: input.clear,
|
|
373
|
-
});
|
|
374
|
-
return {
|
|
375
|
-
content: [{ type: "text", text: "Type successful" }],
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
case "gtkx_fire_event": {
|
|
380
|
-
const input = FireEventInputSchema.parse(args);
|
|
381
|
-
await connectionManager.sendToApp(input.appId, "widget.fireEvent", {
|
|
382
|
-
widgetId: input.widgetId,
|
|
383
|
-
signal: input.signal,
|
|
384
|
-
args: input.args,
|
|
385
|
-
});
|
|
386
|
-
return {
|
|
387
|
-
content: [{ type: "text", text: "Event fired successfully" }],
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
case "gtkx_take_screenshot": {
|
|
392
|
-
const input = TakeScreenshotInputSchema.parse(args);
|
|
393
|
-
const result = await connectionManager.sendToApp<{ data: string; mimeType: string }>(
|
|
394
|
-
input.appId,
|
|
395
|
-
"widget.screenshot",
|
|
396
|
-
{
|
|
397
|
-
windowId: input.windowId,
|
|
398
|
-
},
|
|
399
|
-
);
|
|
400
|
-
return {
|
|
401
|
-
content: [
|
|
402
|
-
{
|
|
403
|
-
type: "image",
|
|
404
|
-
data: result.data,
|
|
405
|
-
mimeType: result.mimeType,
|
|
406
|
-
},
|
|
407
|
-
],
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
default:
|
|
412
|
-
return {
|
|
413
|
-
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
414
|
-
isError: true,
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
} catch (error) {
|
|
418
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
419
|
-
return {
|
|
420
|
-
content: [{ type: "text", text: message }],
|
|
421
|
-
isError: true,
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
const transport = new StdioServerTransport();
|
|
427
|
-
await server.connect(transport);
|
|
428
|
-
|
|
429
|
-
let isShuttingDown = false;
|
|
430
|
-
const shutdown = async () => {
|
|
431
|
-
if (isShuttingDown) return;
|
|
432
|
-
isShuttingDown = true;
|
|
433
|
-
try {
|
|
434
|
-
connectionManager.cleanup();
|
|
435
|
-
await socketServer.stop();
|
|
436
|
-
await server.close();
|
|
437
|
-
} finally {
|
|
438
|
-
process.exit(0);
|
|
439
|
-
}
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
process.on("SIGINT", shutdown);
|
|
443
|
-
process.on("SIGTERM", shutdown);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
main().catch((error) => {
|
|
447
|
-
console.error("[gtkx] Fatal error:", error);
|
|
448
|
-
process.exit(1);
|
|
449
|
-
});
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
import EventEmitter from "node:events";
|
|
2
|
-
import {
|
|
3
|
-
appNotFoundError,
|
|
4
|
-
invalidRequestError,
|
|
5
|
-
ipcTimeoutError,
|
|
6
|
-
McpError,
|
|
7
|
-
type McpErrorCode,
|
|
8
|
-
noAppConnectedError,
|
|
9
|
-
} from "./protocol/errors.js";
|
|
10
|
-
import { type AppInfo, type IpcRequest, type IpcResponse, RegisterParamsSchema } from "./protocol/types.js";
|
|
11
|
-
import type { AppConnection, SocketServer } from "./socket-server.js";
|
|
12
|
-
|
|
13
|
-
type ConnectionManagerEventMap = {
|
|
14
|
-
appRegistered: [AppInfo];
|
|
15
|
-
appUnregistered: [string];
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
type PendingRequest = {
|
|
19
|
-
resolve: (result: unknown) => void;
|
|
20
|
-
reject: (error: Error) => void;
|
|
21
|
-
timeout: NodeJS.Timeout;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
type RegisteredApp = {
|
|
25
|
-
info: AppInfo;
|
|
26
|
-
connection: AppConnection;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Manages connections between the MCP server and GTKX applications.
|
|
31
|
-
*
|
|
32
|
-
* Handles app registration, request routing, and connection lifecycle.
|
|
33
|
-
*/
|
|
34
|
-
export class ConnectionManager extends EventEmitter<ConnectionManagerEventMap> {
|
|
35
|
-
private static readonly DEFAULT_WAIT_TIMEOUT = 10000;
|
|
36
|
-
|
|
37
|
-
private apps: Map<string, RegisteredApp> = new Map();
|
|
38
|
-
private connectionToApp: Map<string, string> = new Map();
|
|
39
|
-
private pendingRequests: Map<string, PendingRequest> = new Map();
|
|
40
|
-
private requestTimeout: number;
|
|
41
|
-
|
|
42
|
-
constructor(
|
|
43
|
-
private socketServer: SocketServer,
|
|
44
|
-
options: { requestTimeout?: number } = {},
|
|
45
|
-
) {
|
|
46
|
-
super();
|
|
47
|
-
this.requestTimeout = options.requestTimeout ?? 30000;
|
|
48
|
-
|
|
49
|
-
this.socketServer.on("request", (connection, request) => {
|
|
50
|
-
this.handleRequest(connection, request);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
this.socketServer.on("response", (_connection, response) => {
|
|
54
|
-
this.handleResponse(response);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
this.socketServer.on("disconnection", (connection) => {
|
|
58
|
-
this.handleDisconnection(connection);
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
getApps(): AppInfo[] {
|
|
63
|
-
return Array.from(this.apps.values()).map((app) => app.info);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
getApp(appId: string): AppInfo | undefined {
|
|
67
|
-
return this.apps.get(appId)?.info;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
hasConnectedApps(): boolean {
|
|
71
|
-
return this.apps.size > 0;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
getDefaultApp(): RegisteredApp | undefined {
|
|
75
|
-
const first = this.apps.values().next();
|
|
76
|
-
return first.done ? undefined : first.value;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Waits for at least one app to connect and register.
|
|
81
|
-
*
|
|
82
|
-
* @param timeout - Maximum time to wait in milliseconds (default: 10000)
|
|
83
|
-
* @returns Promise that resolves with the first registered app info
|
|
84
|
-
* @throws Error if timeout is reached before any app registers
|
|
85
|
-
*/
|
|
86
|
-
waitForApp(timeout: number = ConnectionManager.DEFAULT_WAIT_TIMEOUT): Promise<AppInfo> {
|
|
87
|
-
const defaultApp = this.getDefaultApp();
|
|
88
|
-
if (defaultApp) {
|
|
89
|
-
return Promise.resolve(defaultApp.info);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return new Promise((resolve, reject) => {
|
|
93
|
-
const timeoutId = setTimeout(() => {
|
|
94
|
-
this.off("appRegistered", onRegister);
|
|
95
|
-
reject(
|
|
96
|
-
new Error(
|
|
97
|
-
`Timeout waiting for app registration after ${timeout}ms. ` +
|
|
98
|
-
"Make sure your GTKX app is running with 'gtkx dev'.",
|
|
99
|
-
),
|
|
100
|
-
);
|
|
101
|
-
}, timeout);
|
|
102
|
-
|
|
103
|
-
const onRegister = (appInfo: AppInfo) => {
|
|
104
|
-
clearTimeout(timeoutId);
|
|
105
|
-
this.off("appRegistered", onRegister);
|
|
106
|
-
resolve(appInfo);
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
this.on("appRegistered", onRegister);
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async sendToApp<T>(appId: string | undefined, method: string, params?: unknown): Promise<T> {
|
|
114
|
-
const app = appId ? this.apps.get(appId) : this.getDefaultApp();
|
|
115
|
-
|
|
116
|
-
if (!app) {
|
|
117
|
-
if (appId) {
|
|
118
|
-
throw appNotFoundError(appId);
|
|
119
|
-
}
|
|
120
|
-
throw noAppConnectedError();
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const requestId = crypto.randomUUID();
|
|
124
|
-
const request: IpcRequest = {
|
|
125
|
-
id: requestId,
|
|
126
|
-
method,
|
|
127
|
-
params,
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
return new Promise<T>((resolve, reject) => {
|
|
131
|
-
const timeout = setTimeout(() => {
|
|
132
|
-
this.pendingRequests.delete(requestId);
|
|
133
|
-
reject(ipcTimeoutError(this.requestTimeout));
|
|
134
|
-
}, this.requestTimeout);
|
|
135
|
-
|
|
136
|
-
this.pendingRequests.set(requestId, {
|
|
137
|
-
resolve: resolve as (result: unknown) => void,
|
|
138
|
-
reject,
|
|
139
|
-
timeout,
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
const sent = this.socketServer.send(app.connection.id, request);
|
|
143
|
-
if (!sent) {
|
|
144
|
-
clearTimeout(timeout);
|
|
145
|
-
this.pendingRequests.delete(requestId);
|
|
146
|
-
reject(appNotFoundError(app.info.appId));
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
cleanup(): void {
|
|
153
|
-
for (const pending of this.pendingRequests.values()) {
|
|
154
|
-
clearTimeout(pending.timeout);
|
|
155
|
-
pending.reject(new Error("Connection manager shutting down"));
|
|
156
|
-
}
|
|
157
|
-
this.pendingRequests.clear();
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
private handleRequest(connection: AppConnection, request: IpcRequest): void {
|
|
161
|
-
if (request.method === "app.register") {
|
|
162
|
-
this.handleRegister(connection, request);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (request.method === "app.unregister") {
|
|
167
|
-
this.handleUnregister(connection, request);
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
private handleResponse(response: IpcResponse): void {
|
|
173
|
-
const pending = this.pendingRequests.get(response.id);
|
|
174
|
-
if (!pending) {
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
clearTimeout(pending.timeout);
|
|
179
|
-
this.pendingRequests.delete(response.id);
|
|
180
|
-
|
|
181
|
-
if (response.error) {
|
|
182
|
-
const err = response.error;
|
|
183
|
-
pending.reject(new McpError(err.code as McpErrorCode, err.message, err.data));
|
|
184
|
-
} else {
|
|
185
|
-
pending.resolve(response.result);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
private handleRegister(connection: AppConnection, request: IpcRequest): void {
|
|
190
|
-
const parseResult = RegisterParamsSchema.safeParse(request.params);
|
|
191
|
-
if (!parseResult.success) {
|
|
192
|
-
const response: IpcResponse = {
|
|
193
|
-
id: request.id,
|
|
194
|
-
error: invalidRequestError(parseResult.error.message).toIpcError(),
|
|
195
|
-
};
|
|
196
|
-
this.socketServer.send(connection.id, response);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const params = parseResult.data;
|
|
201
|
-
const appInfo: AppInfo = {
|
|
202
|
-
appId: params.appId,
|
|
203
|
-
pid: params.pid,
|
|
204
|
-
windows: [],
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
const registeredApp: RegisteredApp = {
|
|
208
|
-
info: appInfo,
|
|
209
|
-
connection,
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
this.apps.set(params.appId, registeredApp);
|
|
213
|
-
this.connectionToApp.set(connection.id, params.appId);
|
|
214
|
-
|
|
215
|
-
const response: IpcResponse = {
|
|
216
|
-
id: request.id,
|
|
217
|
-
result: { success: true },
|
|
218
|
-
};
|
|
219
|
-
this.socketServer.send(connection.id, response);
|
|
220
|
-
|
|
221
|
-
this.emit("appRegistered", appInfo);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
private handleUnregister(connection: AppConnection, request: IpcRequest): void {
|
|
225
|
-
const appId = this.connectionToApp.get(connection.id);
|
|
226
|
-
if (appId) {
|
|
227
|
-
this.apps.delete(appId);
|
|
228
|
-
this.connectionToApp.delete(connection.id);
|
|
229
|
-
this.emit("appUnregistered", appId);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const response: IpcResponse = {
|
|
233
|
-
id: request.id,
|
|
234
|
-
result: { success: true },
|
|
235
|
-
};
|
|
236
|
-
this.socketServer.send(connection.id, response);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
private handleDisconnection(connection: AppConnection): void {
|
|
240
|
-
const appId = this.connectionToApp.get(connection.id);
|
|
241
|
-
if (appId) {
|
|
242
|
-
this.apps.delete(appId);
|
|
243
|
-
this.connectionToApp.delete(connection.id);
|
|
244
|
-
this.emit("appUnregistered", appId);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|