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