@gtkx/mcp 2.0.0-beta.10 → 2.0.0-beta.11

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.
@@ -9,7 +9,6 @@ import {
9
9
  } from "./transport.js";
10
10
 
11
11
  class ConnectionRegistry extends EventTarget implements AppConnections {
12
- private connections: Map<string, ProtocolConnection> = new Map();
13
12
  private sockets: Map<string, Socket> = new Map();
14
13
 
15
14
  onRequest: ConnectionRequestHandler = (_connection, request) => Promise.reject(methodNotFoundError(request.method));
@@ -17,14 +16,12 @@ class ConnectionRegistry extends EventTarget implements AppConnections {
17
16
  register(socket: Socket): ProtocolConnection {
18
17
  const connection = ProtocolConnection.fromSocket(socket, {
19
18
  onClose: () => {
20
- this.connections.delete(connection.id);
21
19
  this.sockets.delete(connection.id);
22
20
  this.dispatchEvent(connectionDisconnectionEvent(connection));
23
21
  },
24
22
  onError: (error) => this.dispatchEvent(connectionErrorEvent(error)),
25
23
  });
26
24
 
27
- this.connections.set(connection.id, connection);
28
25
  this.sockets.set(connection.id, socket);
29
26
  connection.fallbackRequestHandler = (request) => this.onRequest(connection, request);
30
27
 
@@ -20,11 +20,7 @@ type SerializedProperty = {
20
20
  note?: string;
21
21
  };
22
22
 
23
- type AppInfo = {
24
- applicationId: string;
25
- pid: number;
26
- projectRoot?: string;
27
- };
23
+ type AppInfo = z.output<typeof RegisterParamsSchema>;
28
24
 
29
25
  type ServerRequestParams<Method extends keyof typeof ServerRequestParamsSchemas> = z.infer<
30
26
  (typeof ServerRequestParamsSchemas)[Method]
package/src/reference.ts CHANGED
@@ -1,5 +1,20 @@
1
- import { type ApiReference, type ApiSymbol, loadApiReference, resolveGirPath, resolveLibraries } from "@gtkx/codegen";
1
+ import {
2
+ type ApiReference,
3
+ type ApiSymbol,
4
+ loadApiReference,
5
+ mergeOmittedProps,
6
+ resolveGirPath,
7
+ resolveLibraries,
8
+ } from "@gtkx/codegen";
9
+ import { readBuiltinElementsForDocs } from "@gtkx/codegen/internal";
2
10
  import { loadConfig } from "@gtkx/config";
11
+ import {
12
+ CONFIG_EXTENSIONS,
13
+ configDependenciesFor,
14
+ resolveAcceptedChildTypes,
15
+ resolveElementProps,
16
+ resolveOmittedProps,
17
+ } from "@gtkx/config/internal";
3
18
  import { type McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
4
19
  import { type CallToolResult, ErrorCode, McpError, type ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
5
20
  import { existsSync, statSync } from "node:fs";
@@ -58,7 +73,7 @@ type ResourceServer = Pick<McpServer, "registerResource">;
58
73
 
59
74
  const FRESHNESS_INTERVAL_MS = 2000;
60
75
  const FAILURE_RETRY_MS = 5000;
61
- const CONFIG_EXTENSIONS = ["ts", "mts", "js", "mjs", "json"];
76
+ const CONFIG_LOCATIONS = ["gtkx.config", ".config/gtkx", ".config/gtkx.config"];
62
77
 
63
78
  const PROJECT_SOURCE_LABELS: Record<ProjectSource, string> = {
64
79
  argument: "requested with `projectRoot`",
@@ -128,10 +143,13 @@ const isFresh = (loaded: LoadedReference): boolean =>
128
143
  const current = watchFile(file.path);
129
144
 
130
145
  return current.mtimeMs === file.mtimeMs && current.size === file.size;
131
- });
146
+ }) && loaded.reference.hasFreshInputs();
147
+
148
+ const configCandidatePaths = (directory: string): string[] =>
149
+ CONFIG_LOCATIONS.flatMap((location) =>
150
+ CONFIG_EXTENSIONS.map((extension) => join(directory, `${location}${extension}`)));
132
151
 
133
- const hasConfigFile = (directory: string): boolean =>
134
- CONFIG_EXTENSIONS.some((extension) => existsSync(join(directory, `gtkx.config.${extension}`)));
152
+ const hasConfigFile = (directory: string): boolean => configCandidatePaths(directory).some((path) => existsSync(path));
135
153
 
136
154
  const findProjectRoot = (start: string): string | undefined => {
137
155
  const current = resolve(start);
@@ -172,7 +190,8 @@ const resolveProject = (
172
190
  };
173
191
 
174
192
  const loadReference = async (requestedRoot: string): Promise<LoadedReference> => {
175
- const { config, configFile, root } = await loadConfig(requestedRoot);
193
+ const loaded = await loadConfig(requestedRoot);
194
+ const { config, root } = loaded;
176
195
 
177
196
  if (config.codegen === false) {
178
197
  throw new Error(
@@ -182,7 +201,7 @@ const loadReference = async (requestedRoot: string): Promise<LoadedReference> =>
182
201
  );
183
202
  }
184
203
 
185
- const girPath = resolveGirPath(config.girPath);
204
+ const girPath = resolveGirPath(config.girPath, root);
186
205
 
187
206
  if (girPath.length === 0) {
188
207
  throw new Error(
@@ -193,13 +212,22 @@ const loadReference = async (requestedRoot: string): Promise<LoadedReference> =>
193
212
  }
194
213
 
195
214
  const libraries = resolveLibraries(config.libraries);
215
+ const builtin = await readBuiltinElementsForDocs();
196
216
 
197
217
  const reference = loadApiReference({
198
218
  libraries,
199
219
  girPath,
220
+ resolveFrom: root,
221
+ props: { ...builtin.props, ...resolveElementProps(config.elements) },
222
+ omittedProps: mergeOmittedProps(builtin.omittedProps, resolveOmittedProps(config.elements)),
223
+ acceptedChildTypes: { ...builtin.acceptedChildTypes, ...resolveAcceptedChildTypes(config.elements) },
200
224
  });
201
225
 
202
- const watched = [watchFile(resolve(root, configFile)), ...reference.girFiles.map((file) => watchFile(file))];
226
+ const watched = [...new Set([
227
+ ...configDependenciesFor(loaded),
228
+ ...configCandidatePaths(root),
229
+ ...reference.inputFiles,
230
+ ])].map((file) => watchFile(file));
203
231
 
204
232
  return { reference, root, watched };
205
233
  };
package/src/server.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
2
  import { loadConfig } from "@gtkx/config";
3
- import { type McpSettings, resolveMcpSettings } from "@gtkx/config/internal";
3
+ import { configDependenciesFor, type McpSettings, resolveMcpSettings } from "@gtkx/config/internal";
4
4
  import { createLogger, installGracefulShutdown, type Logger } from "@gtkx/utils";
5
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { existsSync } from "node:fs";
8
+ import { resolve } from "node:path";
7
9
  import { parseArgs } from "node:util";
8
10
  import { z } from "zod";
9
11
  import type { ConnectionErrorEvent } from "./transport.js";
@@ -175,9 +177,8 @@ const screenshotShape = {
175
177
  .boolean()
176
178
  .optional()
177
179
  .describe(
178
- "Whether to return the PNG as image content, true by default. Pass false together with `path` " +
179
- "to save the screenshot and get back only where it landed, which keeps the image out of the " +
180
- "conversation until something actually needs to look at it.",
180
+ "Return the PNG as image content (default: true). Set false with `path` to save the image " +
181
+ "and return only its file location.",
181
182
  ),
182
183
  };
183
184
 
@@ -296,18 +297,14 @@ const widgetPropsTool = (appRouter: AppRouter): Tool =>
296
297
  title: "Get widget properties",
297
298
  kind: "readOnly",
298
299
  description:
299
- "Get a fixed summary of one widget by ID: type, accessible role, name, text, sensitivity, " +
300
- "visibility, CSS classes, and the same summary for its descendants. The subtree is bounded " +
301
- `twice: ${String(DEFAULT_SUBTREE_DEPTH)} levels deep unless \`maxDepth\` says otherwise, and ` +
302
- `${String(MAX_SUBTREE_WIDGETS)} widgets in all, filled breadth first. Wherever either bound ` +
303
- "cut a branch, that widget carries `hiddenChildren`, the count of its own direct children left " +
304
- "out rather than of everything below them; call again with that widget's ID to drill in, or " +
305
- "use `gtkx_get_widget_tree` for a wider map. Pass `properties` to read GObject properties too; " +
306
- "they come back first in the payload, each under its canonical kebab-case name as " +
307
- "`{type, value}`, where an enum or flags value is its GType value name, a 64-bit integer a " +
308
- "decimal string, and an object its GType name plus `widgetId` when it is a widget. Asking for " +
309
- "a property the widget does not have fails; a value that cannot be marshalled carries a `note` " +
310
- "instead.",
300
+ "Read a widget's type, accessible role, name, text, sensitivity, visibility, CSS classes, " +
301
+ `and descendants. Defaults to ${String(DEFAULT_SUBTREE_DEPTH)} levels; \`maxDepth\` changes ` +
302
+ `the depth. Results contain at most ${String(MAX_SUBTREE_WIDGETS)} widgets, in breadth-first ` +
303
+ "order. `hiddenChildren` counts omitted direct children; query that widget's ID to expand it. " +
304
+ "Use `gtkx_get_widget_tree` for an overview. Requested GObject `properties` appear first, " +
305
+ "keyed by canonical kebab-case name with `{type, value}`. Enum/flags values use GType value " +
306
+ "names, 64-bit integers use decimal strings, and objects use their GType name plus `widgetId` " +
307
+ "for widgets. Unknown properties fail; values that cannot be converted include a `note`.",
311
308
  inputSchema: widgetPropsShape,
312
309
  handler: async ({ applicationId, appTimeout, ...params }) => {
313
310
  const result = await appRouter.sendToApp(applicationId, "widget.getProps", params, appTimeout);
@@ -365,7 +362,7 @@ function buildInteractionTools(appRouter: AppRouter): Tool[] {
365
362
  title: "Click widget",
366
363
  kind: "action",
367
364
  description:
368
- "Click a widget through userEvent.click, with no special case per widget. Works with " +
365
+ "Click a widget through userEvent.click. Works with " +
369
366
  "buttons, checkboxes, switches, list and grid rows, tree expanders, and column headers: a " +
370
367
  "row is selected, an expander toggles its row's expansion, and a header sorts its column.",
371
368
  inputSchema: widgetIdShape,
@@ -539,7 +536,11 @@ const configuredSettings = async (cwd: string): Promise<McpSettings> => {
539
536
  const { config } = await loadConfig(cwd);
540
537
 
541
538
  return resolveMcpSettings(config);
542
- } catch {
539
+ } catch (error) {
540
+ if (configDependenciesFor(error).some((path) => existsSync(resolve(cwd, path)))) {
541
+ throw error;
542
+ }
543
+
543
544
  return DEFAULT_SETTINGS;
544
545
  }
545
546
  };
@@ -2,12 +2,9 @@ import type { McpSettings } from "@gtkx/config/internal";
2
2
  import type { Tool } from "./tool.js";
3
3
 
4
4
  const NEGATION_PREFIX = "!";
5
- const SPECIAL_CHARACTERS = /[.+?^${}()|[\]\\]/g;
6
-
7
- const escapeLiteral = (value: string): string => value.replaceAll(SPECIAL_CHARACTERS, String.raw`\$&`);
8
5
 
9
6
  const patternToRegExp = (pattern: string): RegExp =>
10
- new RegExp(`^${pattern.split("*").map((part) => escapeLiteral(part)).join(".*")}$`);
7
+ new RegExp(`^${pattern.split("*").map((part) => RegExp.escape(part)).join(".*")}$`);
11
8
 
12
9
  const matchingNames = (tools: Tool[], pattern: string): string[] => {
13
10
  const expression = patternToRegExp(pattern);