ucu-mcp 0.3.1 → 0.3.4

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/CHANGELOG.md CHANGED
@@ -5,6 +5,60 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.4] - 2026-06-06
9
+
10
+ ### Re-publish of 0.3.3
11
+
12
+ 0.3.3 was published with a syntax error in the source test file
13
+ (`tests/unit/tools-layer.test.ts:355` was a continuation of a `//`
14
+ comment block that lost its `//` prefix on a hard line break, so
15
+ tsc parsed the second line as a bare identifier). The published
16
+ npm tarball was functionally correct (`dist/` was unaffected), but
17
+ the broken test file would fail to compile for any consumer
18
+ running `npm test` against the source. The fix (a single-character
19
+ comment prefix) was applied, but npm disallows re-publishing the
20
+ same version, so the fixed source ships as 0.3.4 with the same
21
+ contents and CHANGELOG entry as 0.3.3 plus this note.
22
+
23
+ ### Yanked
24
+
25
+ `ucu-mcp@0.3.3` was unpublished (yanked) shortly after release due to
26
+ the test file compile error. Users on `@latest` are now on 0.3.4.
27
+
28
+ ## [0.3.3] - 2026-06-06
29
+
30
+ ### Tests
31
+
32
+ - `tools-layer`: three new test cases cover `wait_for_element` value+textMode combinations (contains, exact, regex). They confirm the response surfaces the matched value unchanged so the model can branch on it. (Completes Singer Item 4)
33
+
34
+ ### Refactor
35
+
36
+ - JXA `textMatches` and `valueMatches` consolidated through a shared `matchesValue(filter, value, mode)` helper. No behavior change; the three branches (contains / exact / regex) now live in one place. (Completes Singer Item 8)
37
+
38
+ ## [0.3.2] - 2026-06-06
39
+
40
+ ### Bug fixes
41
+
42
+ - `find_element` with `textMode="regex"` now pre-validates the `value` field for invalid regex patterns and throws `PlatformError`, mirroring the existing `text`-field validation. Before, an invalid value regex was silently swallowed by the JXA-internal `try/catch` and surfaced as "no results" instead of a clear error. (Singer Minor)
43
+ - `find_element` `near` sort now explicitly pushes elements without `bounds` to the end of the sorted result, instead of implicitly treating them as centered at (0,0). Improves semantics for elements without on-screen geometry. (Singer Nit)
44
+
45
+ ### Changed
46
+
47
+ - `find_element.value` schema is now `z.string().min(1).optional()`. Empty strings are now rejected at the schema layer with a clear validation error rather than being silently coerced to "no filter". (Singer Minor)
48
+
49
+ ### Tests
50
+
51
+ - `macos-platform`: the `index out of range` test now also pins `metrics.matchedCount` to the JXA return value, locking the semantic that out-of-range indexing does not change the underlying match count. (Singer Minor)
52
+
53
+ ### Tool description
54
+
55
+ - `find_element` tool description expanded to mention `value` / `index` / `near` selector support, so the model sees the new selectors at the tool level rather than only on individual parameters. (Singer Minor)
56
+ - `UcuError.defaultCode` lookup now has a JSDoc cross-reference explaining the relationship between the static class default and the per-instance `code` field. (Singer Minor)
57
+
58
+ ### Hygiene
59
+
60
+ - Tracked 7 files removed from git tracking: `.codex/{config.toml,postmortem-interrupt-loop.md}`, `.claude/{settings.json,settings.local.json,.cozempic-init.lock}`, `docs/{.DS_Store,superpowers/.DS_Store}`. These were local-environment residue that predated the `.gitignore` rules; the ignore rules were already in place, just not enforced on the existing tracked entries. `claude-desktop-config.json` (the root-level sample for Claude Desktop MCP setup) was kept.
61
+
8
62
  ## [0.3.1] - 2026-06-06
9
63
 
10
64
  ### Bug fixes
@@ -547,12 +547,12 @@ export function registerTools(server) {
547
547
  return actionResponse("move", { moved: true, x: pt.x, y: pt.y }, { x: pt.x, y: pt.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth);
548
548
  });
549
549
  registry.register("move");
550
- registerTool("find_element", "Find accessibility elements by text, role, or app", {
550
+ registerTool("find_element", "Find accessibility elements by text, role, or value. Supports value/index/near selectors.", {
551
551
  text: z.string().optional().describe("Text to search"), role: z.string().optional().describe("AX role"), app: z.string().optional().describe("Target app"),
552
552
  depth: z.number().optional().describe("AX tree depth"), includeBounds: z.boolean().default(true).describe("Include bounds"), maxResults: z.number().min(1).max(200).default(50).describe("Max results"),
553
553
  textMode: z.enum(["contains", "exact", "regex"]).default("contains").describe("Text matching mode: contains (default), exact, or regex"),
554
554
  visibleOnly: z.boolean().default(false).describe("Only return elements with valid on-screen bounds"),
555
- value: z.string().optional().describe("Filter by AX element value (respects textMode)"),
555
+ value: z.string().min(1).optional().describe("Filter by AX element value (text/regex/exact, see textMode). Empty string is treated as unset (omit the field instead)."),
556
556
  index: z.number().int().nonnegative().optional().describe("Return only the Nth match (0-based) after all other filtering and sorting"),
557
557
  near: z.object({ x: z.number(), y: z.number() }).optional().describe("Sort results by ascending distance to this point and return closest first"),
558
558
  }, async (params) => {
@@ -820,6 +820,17 @@ export class MacOSPlatform {
820
820
  throw new PlatformError(`Invalid regex pattern: ${text}`);
821
821
  }
822
822
  }
823
+ // Same pre-validation for value field when regex textMode is requested;
824
+ // otherwise JXA's valueMatches silently returns false on invalid regex,
825
+ // which surfaces as "no results" instead of a clear error.
826
+ if (value && textMode === "regex") {
827
+ try {
828
+ new RegExp(value);
829
+ }
830
+ catch {
831
+ throw new PlatformError(`Invalid regex pattern: ${value}`);
832
+ }
833
+ }
823
834
  const startTime = Date.now();
824
835
  const jxaScript = `
825
836
  var se = Application('System Events');
@@ -852,45 +863,37 @@ export class MacOSPlatform {
852
863
  }
853
864
  }
854
865
 
855
- function textMatches(elemName, elemValue, elemDesc) {
856
- if (textFilter === null) return true;
857
- var sources = [elemName, elemValue, elemDesc];
858
- if (textMode === "exact") {
859
- var t = textFilter.toLowerCase();
860
- for (var i = 0; i < sources.length; i++) {
861
- if (sources[i].toLowerCase() === t) return true;
862
- }
863
- return false;
864
- } else if (textMode === "regex") {
866
+ // Shared filter helper. textMatches and valueMatches used to be near
867
+ // copies of the same three-branch dispatch (contains / exact / regex);
868
+ // this consolidates the logic so the two callers only differ in which
869
+ // sources they iterate. Declared before textMatches/valueMatches
870
+ // because JXA function declarations are order-sensitive within a
871
+ // script. (Singer Nit)
872
+ function matchesValue(filter, value, mode) {
873
+ if (filter === null) return true;
874
+ if (mode === "exact") {
875
+ return value.toLowerCase() === filter.toLowerCase();
876
+ } else if (mode === "regex") {
865
877
  try {
866
- var re = new RegExp(textFilter, "i");
867
- for (var i = 0; i < sources.length; i++) {
868
- if (re.test(sources[i])) return true;
869
- }
870
- } catch(e) {}
871
- return false;
878
+ return new RegExp(filter, "i").test(value);
879
+ } catch(e) { return false; }
872
880
  } else {
873
881
  // contains (default)
874
- var t = textFilter.toLowerCase();
875
- for (var i = 0; i < sources.length; i++) {
876
- if (sources[i].toLowerCase().indexOf(t) !== -1) return true;
877
- }
878
- return false;
882
+ return value.toLowerCase().indexOf(filter.toLowerCase()) !== -1;
879
883
  }
880
884
  }
881
885
 
882
- function valueMatches(elemValue) {
883
- if (valueFilter === null) return true;
884
- if (textMode === "exact") {
885
- return elemValue.toLowerCase() === valueFilter.toLowerCase();
886
- } else if (textMode === "regex") {
887
- try {
888
- return new RegExp(valueFilter, "i").test(elemValue);
889
- } catch(e) { return false; }
890
- } else {
891
- // contains (default)
892
- return elemValue.toLowerCase().indexOf(valueFilter.toLowerCase()) !== -1;
886
+ function textMatches(elemName, elemValue, elemDesc) {
887
+ if (textFilter === null) return true;
888
+ var sources = [elemName, elemValue, elemDesc];
889
+ for (var i = 0; i < sources.length; i++) {
890
+ if (matchesValue(textFilter, sources[i], textMode)) return true;
893
891
  }
892
+ return false;
893
+ }
894
+
895
+ function valueMatches(elemValue) {
896
+ return matchesValue(valueFilter, elemValue, textMode);
894
897
  }
895
898
 
896
899
  function matches(elem) {
@@ -1024,6 +1027,17 @@ export class MacOSPlatform {
1024
1027
  const nx = options.near.x;
1025
1028
  const ny = options.near.y;
1026
1029
  finalResults = [...finalResults].sort((a, b) => {
1030
+ // Elements without bounds cannot be meaningfully compared against
1031
+ // a near point. Push them to the end of the sorted result so they
1032
+ // don't pollute the "closest first" ordering. (Singer Nit)
1033
+ const aHasBounds = !!a.bounds;
1034
+ const bHasBounds = !!b.bounds;
1035
+ if (!aHasBounds && !bHasBounds)
1036
+ return 0;
1037
+ if (!aHasBounds)
1038
+ return 1;
1039
+ if (!bHasBounds)
1040
+ return -1;
1027
1041
  const acx = (a.bounds?.x ?? 0) + (a.bounds?.width ?? 0) / 2;
1028
1042
  const acy = (a.bounds?.y ?? 0) + (a.bounds?.height ?? 0) / 2;
1029
1043
  const bcx = (b.bounds?.x ?? 0) + (b.bounds?.width ?? 0) / 2;
@@ -16,6 +16,8 @@ export class UcuError extends Error {
16
16
  constructor(message, code, retryable = false) {
17
17
  super(message);
18
18
  if (code === undefined) {
19
+ // The default code applied to instances of this class when no explicit code is passed to the constructor.
20
+ // See the static `defaultCode` declaration above for the per-class override mechanism.
19
21
  code = this.constructor.defaultCode;
20
22
  }
21
23
  this.name = this.constructor.name;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucu-mcp",
3
- "version": "0.3.1",
3
+ "version": "0.3.4",
4
4
  "description": "MCP server for Universal Computer Use — desktop automation for AI agents via Model Context Protocol",
5
5
  "type": "module",
6
6
  "bin": {