@gtkx/mcp 1.0.0-rc.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -9
- package/dist/internal.d.ts +2 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -2
- package/dist/internal.js.map +1 -1
- package/dist/protocol/errors.d.ts +3 -1
- package/dist/protocol/errors.d.ts.map +1 -1
- package/dist/protocol/errors.js +9 -1
- package/dist/protocol/errors.js.map +1 -1
- package/dist/protocol/schemas.d.ts +18 -4
- package/dist/protocol/schemas.d.ts.map +1 -1
- package/dist/protocol/schemas.js +10 -3
- package/dist/protocol/schemas.js.map +1 -1
- package/dist/reference.d.ts +17 -3
- package/dist/reference.d.ts.map +1 -1
- package/dist/reference.js +123 -64
- package/dist/reference.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +120 -57
- package/dist/server.js.map +1 -1
- package/dist/socket-server.d.ts +10 -1
- package/dist/socket-server.d.ts.map +1 -1
- package/dist/socket-server.js +234 -33
- package/dist/socket-server.js.map +1 -1
- package/dist/transport.d.ts +1 -1
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js.map +1 -1
- package/package.json +4 -4
- package/src/internal.ts +4 -0
- package/src/protocol/errors.ts +14 -0
- package/src/protocol/schemas.ts +32 -6
- package/src/reference.ts +206 -83
- package/src/server.ts +162 -72
- package/src/socket-server.ts +309 -40
- package/src/transport.ts +0 -1
package/src/server.ts
CHANGED
|
@@ -9,14 +9,22 @@ import { ConnectionRegistry } from "./connection-registry.js";
|
|
|
9
9
|
import {
|
|
10
10
|
type AppInfo,
|
|
11
11
|
DEFAULT_SOCKET_PATH,
|
|
12
|
+
DEFAULT_SUBTREE_DEPTH,
|
|
12
13
|
fireEventParams,
|
|
14
|
+
MAX_SUBTREE_WIDGETS,
|
|
13
15
|
queryParams,
|
|
14
16
|
screenshotParams,
|
|
15
17
|
treeParams,
|
|
16
18
|
typeParams,
|
|
17
19
|
widgetIdParams,
|
|
20
|
+
widgetPropsParams,
|
|
18
21
|
} from "./protocol/schemas.js";
|
|
19
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
buildReferenceTools,
|
|
24
|
+
createReferenceProvider,
|
|
25
|
+
type ReferenceProvider,
|
|
26
|
+
registerReferenceResources,
|
|
27
|
+
} from "./reference.js";
|
|
20
28
|
import { SocketServer } from "./socket-server.js";
|
|
21
29
|
import { defineTool, imageContent, registerTool, textContent, type Tool } from "./tool.js";
|
|
22
30
|
|
|
@@ -30,6 +38,14 @@ type McpServerHandle = {
|
|
|
30
38
|
stop(): Promise<void>;
|
|
31
39
|
};
|
|
32
40
|
|
|
41
|
+
type ServerLifecycle = {
|
|
42
|
+
socketServer: SocketServer;
|
|
43
|
+
mcpServer: McpServer;
|
|
44
|
+
socketPath: string;
|
|
45
|
+
isStopped: boolean;
|
|
46
|
+
isStarted: boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
33
49
|
type AppWindow = { id: string; title: string | null };
|
|
34
50
|
type AppWithWindows = AppInfo & { windows?: AppWindow[] };
|
|
35
51
|
|
|
@@ -47,18 +63,34 @@ const applicationIdShape = { applicationId: z.string().optional().describe(APPLI
|
|
|
47
63
|
|
|
48
64
|
const widgetIdShape = {
|
|
49
65
|
...applicationIdShape,
|
|
50
|
-
|
|
66
|
+
...describeParams(widgetIdParams.shape, { widgetId: WIDGET_ID_DESCRIPTION }),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const widgetPropsShape = {
|
|
70
|
+
...applicationIdShape,
|
|
71
|
+
...describeParams(widgetPropsParams.shape, {
|
|
72
|
+
widgetId: WIDGET_ID_DESCRIPTION,
|
|
73
|
+
properties:
|
|
74
|
+
"GObject property names to read as well, in kebab-case or camelCase (\"current-breakpoint\" or " +
|
|
75
|
+
"\"currentBreakpoint\"). Omit for the summary alone.",
|
|
76
|
+
maxDepth:
|
|
77
|
+
`How many levels of descendants to include: ${String(DEFAULT_SUBTREE_DEPTH)} by default, and 0 ` +
|
|
78
|
+
`for the widget on its own. At most ${String(MAX_SUBTREE_WIDGETS)} widgets come back whatever ` +
|
|
79
|
+
"the depth, and any widget whose own direct children were left out carries a `hiddenChildren` count.",
|
|
80
|
+
}),
|
|
51
81
|
};
|
|
52
82
|
|
|
53
83
|
const treeShape = {
|
|
54
84
|
...applicationIdShape,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
85
|
+
...describeParams(treeParams.shape, {
|
|
86
|
+
rootId:
|
|
87
|
+
"Render only the subtree rooted at this widget ID (from a prior tree or query). Omit for the " +
|
|
88
|
+
"whole app.",
|
|
89
|
+
maxDepth:
|
|
90
|
+
"Limit how many levels deep to render, and 0 for the root widget on its own; deeper " +
|
|
91
|
+
"descendants are summarized with a count. Combine with rootId to drill in without dumping " +
|
|
92
|
+
"the whole tree.",
|
|
93
|
+
}),
|
|
62
94
|
};
|
|
63
95
|
|
|
64
96
|
const listAppsShape = {
|
|
@@ -73,32 +105,58 @@ const listAppsShape = {
|
|
|
73
105
|
|
|
74
106
|
const queryWidgetsShape = {
|
|
75
107
|
...applicationIdShape,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
108
|
+
...describeParams(queryParams.shape, {
|
|
109
|
+
by: "Query type",
|
|
110
|
+
value: "Value to search for",
|
|
111
|
+
options: "Additional query options",
|
|
112
|
+
}),
|
|
79
113
|
};
|
|
80
114
|
|
|
81
115
|
const typeShape = {
|
|
82
|
-
...
|
|
83
|
-
|
|
84
|
-
|
|
116
|
+
...applicationIdShape,
|
|
117
|
+
...describeParams(typeParams.shape, {
|
|
118
|
+
widgetId: WIDGET_ID_DESCRIPTION,
|
|
119
|
+
text: "Text to type",
|
|
120
|
+
clear: "Clear existing text before typing",
|
|
121
|
+
}),
|
|
85
122
|
};
|
|
86
123
|
|
|
87
124
|
const fireEventShape = {
|
|
88
|
-
...
|
|
89
|
-
|
|
90
|
-
|
|
125
|
+
...applicationIdShape,
|
|
126
|
+
...describeParams(fireEventParams.shape, {
|
|
127
|
+
widgetId: WIDGET_ID_DESCRIPTION,
|
|
128
|
+
signal: "GTK4 signal name to emit",
|
|
129
|
+
args: "Arguments to pass to the signal",
|
|
130
|
+
}),
|
|
91
131
|
};
|
|
92
132
|
|
|
93
133
|
const screenshotShape = {
|
|
94
134
|
...applicationIdShape,
|
|
95
|
-
|
|
96
|
-
"Window ID to capture. If not specified, captures the first window.",
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
135
|
+
...describeParams(screenshotParams.shape, {
|
|
136
|
+
windowId: "Window ID to capture. If not specified, captures the first window.",
|
|
137
|
+
path:
|
|
138
|
+
"Absolute path to write the PNG to on the app's machine. If set, the screenshot is saved there " +
|
|
139
|
+
"in addition to being returned.",
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
function describeParams<Shape extends Record<string, z.ZodType>>(
|
|
144
|
+
shape: Shape,
|
|
145
|
+
descriptions: { [Key in keyof Shape]: string },
|
|
146
|
+
): Shape {
|
|
147
|
+
const described: [string, z.ZodType][] = Object.entries(shape).map(([key, schema]) => [
|
|
148
|
+
key,
|
|
149
|
+
schema.describe(descriptions[key as keyof Shape]),
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
return Object.fromEntries(described) as Shape;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const connectStdio = async (mcpServer: McpServer, stop: () => Promise<void>): Promise<void> => {
|
|
156
|
+
const transport = new StdioServerTransport();
|
|
157
|
+
process.stdin.on("end", () => void stop());
|
|
158
|
+
process.stdin.on("close", () => void stop());
|
|
159
|
+
await mcpServer.connect(transport);
|
|
102
160
|
};
|
|
103
161
|
|
|
104
162
|
const logSocketError = (event: Event): void => {
|
|
@@ -175,6 +233,32 @@ const screenshotTool = (appRouter: AppRouter): Tool =>
|
|
|
175
233
|
},
|
|
176
234
|
});
|
|
177
235
|
|
|
236
|
+
const widgetPropsTool = (appRouter: AppRouter): Tool =>
|
|
237
|
+
defineTool({
|
|
238
|
+
name: "gtkx_get_widget_props",
|
|
239
|
+
title: "Get widget properties",
|
|
240
|
+
kind: "readOnly",
|
|
241
|
+
description:
|
|
242
|
+
"Get a fixed summary of one widget by ID: type, accessible role, name, text, sensitivity, " +
|
|
243
|
+
"visibility, CSS classes, and the same summary for its descendants. The subtree is bounded " +
|
|
244
|
+
`twice: ${String(DEFAULT_SUBTREE_DEPTH)} levels deep unless \`maxDepth\` says otherwise, and ` +
|
|
245
|
+
`${String(MAX_SUBTREE_WIDGETS)} widgets in all, filled breadth first. Wherever either bound ` +
|
|
246
|
+
"cut a branch, that widget carries `hiddenChildren`, the count of its own direct children left " +
|
|
247
|
+
"out rather than of everything below them; call again with that widget's ID to drill in, or " +
|
|
248
|
+
"use `gtkx_get_widget_tree` for a wider map. Pass `properties` to read GObject properties too; " +
|
|
249
|
+
"they come back first in the payload, each under its canonical kebab-case name as " +
|
|
250
|
+
"`{type, value}`, where an enum or flags value is its GType value name, a 64-bit integer a " +
|
|
251
|
+
"decimal string, and an object its GType name plus `widgetId` when it is a widget. Asking for " +
|
|
252
|
+
"a property the widget does not have fails; a value that cannot be marshalled carries a `note` " +
|
|
253
|
+
"instead.",
|
|
254
|
+
inputSchema: widgetPropsShape,
|
|
255
|
+
handler: async ({ applicationId, ...params }) => {
|
|
256
|
+
const result = await appRouter.sendToApp(applicationId, "widget.getProps", params);
|
|
257
|
+
|
|
258
|
+
return textContent(JSON.stringify(result, null, 2));
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
|
|
178
262
|
function buildInspectionTools(appRouter: AppRouter): Tool[] {
|
|
179
263
|
return [
|
|
180
264
|
listAppsTool(appRouter),
|
|
@@ -201,7 +285,10 @@ function buildInspectionTools(appRouter: AppRouter): Tool[] {
|
|
|
201
285
|
title: "Query widgets",
|
|
202
286
|
kind: "readOnly",
|
|
203
287
|
description:
|
|
204
|
-
"Find widgets by role, text, name, or label. Returns
|
|
288
|
+
"Find widgets by role, text, name, or label. Returns each match with its ID and the same " +
|
|
289
|
+
"fixed summary `gtkx_get_widget_props` returns, with no descendants: a match that has " +
|
|
290
|
+
"children carries `hiddenChildren`, the count of its direct children left out. Read a " +
|
|
291
|
+
"match's subtree with `gtkx_get_widget_props` or `gtkx_get_widget_tree`.",
|
|
205
292
|
inputSchema: queryWidgetsShape,
|
|
206
293
|
handler: async ({ applicationId, ...params }) => {
|
|
207
294
|
const result = await appRouter.sendToApp(applicationId, "widget.query", params);
|
|
@@ -209,21 +296,7 @@ function buildInspectionTools(appRouter: AppRouter): Tool[] {
|
|
|
209
296
|
return textContent(JSON.stringify(result, null, 2));
|
|
210
297
|
},
|
|
211
298
|
}),
|
|
212
|
-
|
|
213
|
-
name: "gtkx_get_widget_props",
|
|
214
|
-
title: "Get widget properties",
|
|
215
|
-
kind: "readOnly",
|
|
216
|
-
description:
|
|
217
|
-
"Get a fixed summary of one widget by ID: type, accessible role, name, text, sensitivity, " +
|
|
218
|
-
"visibility, CSS classes, and the full subtree of descendant widgets. It does not return " +
|
|
219
|
-
"arbitrary GObject properties.",
|
|
220
|
-
inputSchema: widgetIdShape,
|
|
221
|
-
handler: async ({ applicationId, ...params }) => {
|
|
222
|
-
const result = await appRouter.sendToApp(applicationId, "widget.getProps", params);
|
|
223
|
-
|
|
224
|
-
return textContent(JSON.stringify(result, null, 2));
|
|
225
|
-
},
|
|
226
|
-
}),
|
|
299
|
+
widgetPropsTool(appRouter),
|
|
227
300
|
screenshotTool(appRouter),
|
|
228
301
|
];
|
|
229
302
|
}
|
|
@@ -234,7 +307,10 @@ function buildInteractionTools(appRouter: AppRouter): Tool[] {
|
|
|
234
307
|
name: "gtkx_click",
|
|
235
308
|
title: "Click widget",
|
|
236
309
|
kind: "action",
|
|
237
|
-
description:
|
|
310
|
+
description:
|
|
311
|
+
"Click a widget through userEvent.click, with no special case per widget. Works with " +
|
|
312
|
+
"buttons, checkboxes, switches, list and grid rows, tree expanders, and column headers: a " +
|
|
313
|
+
"row is selected, an expander toggles its row's expansion, and a header sorts its column.",
|
|
238
314
|
inputSchema: widgetIdShape,
|
|
239
315
|
handler: async ({ applicationId, ...params }) => {
|
|
240
316
|
await appRouter.sendToApp(applicationId, "widget.click", params);
|
|
@@ -261,9 +337,9 @@ function buildInteractionTools(appRouter: AppRouter): Tool[] {
|
|
|
261
337
|
description: "Emit a GTK4 signal on a widget. Use this for custom interactions.",
|
|
262
338
|
inputSchema: fireEventShape,
|
|
263
339
|
handler: async ({ applicationId, ...params }) => {
|
|
264
|
-
await appRouter.sendToApp(applicationId, "widget.fireEvent", params);
|
|
340
|
+
const result = await appRouter.sendToApp(applicationId, "widget.fireEvent", params);
|
|
265
341
|
|
|
266
|
-
return textContent(
|
|
342
|
+
return textContent(JSON.stringify(result, null, 2));
|
|
267
343
|
},
|
|
268
344
|
}),
|
|
269
345
|
];
|
|
@@ -273,6 +349,44 @@ function buildTools(appRouter: AppRouter): Tool[] {
|
|
|
273
349
|
return [...buildInspectionTools(appRouter), ...buildInteractionTools(appRouter)];
|
|
274
350
|
}
|
|
275
351
|
|
|
352
|
+
const registerTools = (mcpServer: McpServer, appRouter: AppRouter, provider: ReferenceProvider): void => {
|
|
353
|
+
for (const tool of [...buildTools(appRouter), ...buildReferenceTools(provider)]) {
|
|
354
|
+
registerTool(mcpServer, tool);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
async function stopServer(state: ServerLifecycle): Promise<void> {
|
|
359
|
+
if (state.isStopped) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
state.isStopped = true;
|
|
364
|
+
await state.socketServer.stop();
|
|
365
|
+
await state.mcpServer.close();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function startServer(state: ServerLifecycle): Promise<void> {
|
|
369
|
+
if (state.isStopped) {
|
|
370
|
+
throw new Error("createMcpServer: a stopped server cannot be started again");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
await state.socketServer.start();
|
|
374
|
+
|
|
375
|
+
if (state.isStarted) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
state.isStarted = true;
|
|
380
|
+
log.info(`socket server listening on ${state.socketPath}`);
|
|
381
|
+
await connectStdio(state.mcpServer, () => stopServer(state));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const createServerHandle = (socketServer: SocketServer, mcpServer: McpServer, socketPath: string): McpServerHandle => {
|
|
385
|
+
const state: ServerLifecycle = { socketServer, mcpServer, socketPath, isStopped: false, isStarted: false };
|
|
386
|
+
|
|
387
|
+
return { start: () => startServer(state), stop: () => stopServer(state) };
|
|
388
|
+
};
|
|
389
|
+
|
|
276
390
|
const createMcpServer = (options: CreateMcpServerOptions): McpServerHandle => {
|
|
277
391
|
const socketPath = options.socketPath ?? DEFAULT_SOCKET_PATH;
|
|
278
392
|
const registry = new ConnectionRegistry();
|
|
@@ -290,35 +404,11 @@ const createMcpServer = (options: CreateMcpServerOptions): McpServerHandle => {
|
|
|
290
404
|
});
|
|
291
405
|
|
|
292
406
|
const mcpServer = new McpServer({ name: "gtkx-mcp", version: options.version });
|
|
293
|
-
const referenceProvider = createReferenceProvider(() => appRouter.getProjectRoot()
|
|
294
|
-
|
|
295
|
-
for (const tool of [...buildTools(appRouter), ...buildReferenceTools(referenceProvider)]) {
|
|
296
|
-
registerTool(mcpServer, tool);
|
|
297
|
-
}
|
|
298
|
-
|
|
407
|
+
const referenceProvider = createReferenceProvider({ getAppRoot: () => appRouter.getProjectRoot() });
|
|
408
|
+
registerTools(mcpServer, appRouter, referenceProvider);
|
|
299
409
|
registerReferenceResources(mcpServer, referenceProvider);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const stop = async (): Promise<void> => {
|
|
303
|
-
if (isStopped) {
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
isStopped = true;
|
|
308
|
-
await socketServer.stop();
|
|
309
|
-
await mcpServer.close();
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
const start = async (): Promise<void> => {
|
|
313
|
-
await socketServer.start();
|
|
314
|
-
log.info(`socket server listening on ${socketPath}`);
|
|
315
|
-
const transport = new StdioServerTransport();
|
|
316
|
-
process.stdin.on("end", () => void stop());
|
|
317
|
-
process.stdin.on("close", () => void stop());
|
|
318
|
-
await mcpServer.connect(transport);
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
return { start, stop };
|
|
410
|
+
|
|
411
|
+
return createServerHandle(socketServer, mcpServer, socketPath);
|
|
322
412
|
};
|
|
323
413
|
|
|
324
414
|
async function main(): Promise<void> {
|