machine-bridge-mcp 0.12.0 → 0.12.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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.1 - 2026-07-12
4
+
5
+ ### Real local automation corrections
6
+
7
+ - Make the fixed macOS JXA helper return JSON as the top-level `osascript` result instead of using `console.log`, which real `osascript` sends to stderr. Empty helper stdout now fails explicitly rather than being interpreted as a successful empty object.
8
+ - Skip recursive macOS menu-bar/menu traversal by default so bounded inspection reaches main-window controls promptly. Add `include_menus` to inspection and actions for explicit menu automation.
9
+ - Normalize hyphens, underscores, dots, and spaces during capability ranking, and strongly boost explicitly named skills and commands. Natural-language references such as `agents progressive disclosure` now select `agents-progressive-disclosure` instead of a generic competing skill.
10
+
11
+ ### Live verification and regression coverage
12
+
13
+ - Add regression coverage for missing JXA output, menu-recursion forwarding, and punctuation-normalized Skill selection. Add an opt-in real macOS Calculator smoke path and document isolated-profile MV3 browser validation.
14
+ - Validate the source runtime with a temporary real global instruction file, an existing user Skill, a registered direct-argv command, shell execution, Calculator discovery/open/activation/inspection/click, and a real unpacked browser extension controlling a local form through inspect/fill/click/source/screenshot operations. Test files, browser profiles, temporary global configuration, tabs, and application state are removed afterward.
15
+
3
16
  ## 0.12.0 - 2026-07-12
4
17
 
5
18
  ### Locking, persistence, and process lifecycle
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Machine Bridge Browser",
4
- "version": "0.12.0",
4
+ "version": "0.12.1",
5
5
  "description": "Connects the current Chromium browser profile to the local Machine Bridge runtime for user-authorized automation.",
6
6
  "permissions": [
7
7
  "tabs",
@@ -29,5 +29,5 @@
29
29
  "action": {
30
30
  "default_title": "Machine Bridge Browser"
31
31
  },
32
- "version_name": "0.12.0"
32
+ "version_name": "0.12.1"
33
33
  }
@@ -189,7 +189,7 @@ No persistent skill or project-context index is trusted as authoritative. `sessi
189
189
 
190
190
  ## Progressive disclosure and task selection
191
191
 
192
- `agent_context` returns bounded skill metadata. `load_local_skill` returns full instructions only for one selected bundle. `resolve_task_capabilities` tokenizes the current task, ranks skill names/descriptions and command names/descriptions/argv, returns matches with scores, and loads the leading skill only when its relevance threshold is met.
192
+ `agent_context` returns bounded skill metadata. `load_local_skill` returns full instructions only for one selected bundle. `resolve_task_capabilities` tokenizes the current task, ranks skill names/descriptions and command names/descriptions/argv, returns matches with scores, and loads the leading skill only when its relevance threshold is met. Hyphens, underscores, dots, and whitespace are normalized for matching, while an explicitly named skill or registered command receives a strong deterministic boost. Thus a task that says “agents progressive disclosure” can match `agents-progressive-disclosure` without relying on generic description words.
193
193
 
194
194
  This ranking is deterministic local assistance, not semantic certainty. The model must still evaluate whether the selected skill applies. Machine Bridge does not execute skill scripts implicitly and does not fabricate a dynamically named MCP tool per skill.
195
195
 
@@ -83,7 +83,9 @@ Then use `value_resource` for a text field or `resources` for `browser_upload_fi
83
83
  - `inspect_local_application` returns a bounded macOS Accessibility tree.
84
84
  - `operate_local_application` performs a structured Accessibility action.
85
85
 
86
- macOS UI inspection and actions require Accessibility permission for the Node/Machine Bridge process. Application discovery and opening are available on supported desktop platforms; structured UI inspection currently targets macOS. The application backend uses fixed JXA implementation code. The caller selects only an application, structured selector, action, and optional bounded value; NUL-containing action text is rejected. `include_values` never returns values for secure-text roles or controls whose metadata indicates passwords, tokens, one-time codes, or payment-card secrets.
86
+ macOS UI inspection and actions require Accessibility permission for the Node/Machine Bridge process. Application discovery and opening are available on supported desktop platforms; structured UI inspection currently targets macOS. The application backend uses fixed JXA implementation code and requires a non-empty JSON result from `osascript`; missing or malformed helper output fails explicitly. The caller selects only an application, structured selector, action, and optional bounded value; NUL-containing action text is rejected. `include_values` never returns values for secure-text roles or controls whose metadata indicates passwords, tokens, one-time codes, or payment-card secrets.
87
+
88
+ Menu-bar and menu subtrees are not recursively expanded by default. This keeps main-window controls within the element/time budget for applications with large localized menu hierarchies. Pass `include_menus: true` to `inspect_local_application` or `operate_local_application` only when the target is a menu item.
87
89
 
88
90
  ## Capability discovery and automatic selection
89
91
 
package/docs/TESTING.md CHANGED
@@ -49,6 +49,18 @@ The suite includes:
49
49
  - live stdio MCP initialization with session instructions, capability resolution, discovery, calls, rich content, sessions, cancellation, managed-job acceptance, and a detached job/finally phase that survives stdio shutdown;
50
50
  - live local Worker OAuth registration, consent, PKCE, token replay rejection, throttling, CORS, protocol negotiation, daemon-backed session bootstrap, dynamic tool advertisement, rich content, daemon replacement, cancellation, malformed daemon JSON/non-object rejection, duplicate hello rejection, and unknown-message closure.
51
51
 
52
+ ## Opt-in live desktop and browser validation
53
+
54
+ The normal suite uses deterministic mocks for macOS Accessibility and an authenticated in-process extension peer for browser routing. Before a release that changes local UI automation, run the macOS live smoke test on a machine where the invoking Node/terminal process has Accessibility permission:
55
+
56
+ ```sh
57
+ npm run app-automation:live-test
58
+ ```
59
+
60
+ It opens Calculator, activates it through the fixed JXA helper, verifies structured JSON output, inspects main-window Accessibility controls with menu recursion disabled, clicks the `One` control, and quits the application. It is intentionally excluded from CI because macOS TCC permission and a graphical login session are host state.
61
+
62
+ For browser changes, perform an isolated-profile smoke test with the packaged unpacked extension: pair it to a temporary source runtime, navigate a local no-store page, inspect controls, fill multiple fields, click a button, verify the live DOM, and capture a screenshot. A Playwright persistent Chromium context is an acceptable harness because it loads the real MV3 service worker while keeping the user's daily browser profile untouched.
63
+
52
64
  ## Additional release checks
53
65
 
54
66
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "machine-bridge-mcp",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Cross-client MCP bridge for local agent context, structured browser and application automation, files, Git, processes, resources, and durable jobs over stdio or OAuth relay.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -69,7 +69,8 @@
69
69
  "browser-bridge:test": "node tests/browser-bridge-test.mjs",
70
70
  "app-automation:test": "node tests/app-automation-test.mjs",
71
71
  "process-lock:test": "node tests/process-lock-test.mjs",
72
- "service-lifecycle:test": "node tests/service-lifecycle-test.mjs"
72
+ "service-lifecycle:test": "node tests/service-lifecycle-test.mjs",
73
+ "app-automation:live-test": "node tests/app-automation-test.mjs --live-macos"
73
74
  },
74
75
  "dependencies": {
75
76
  "wrangler": "4.110.0",
@@ -112,12 +112,12 @@ export class AgentContextManager {
112
112
  const discovered = await this.discoverSkills(state, { maxResults: MAX_SKILL_RESULTS }, context);
113
113
  const maxSkills = clampInt(args.max_skills, 10, 1, 50);
114
114
  const skillMatches = discovered.skills
115
- .map((skill) => ({ skill, score: relevanceScore(task, `${skill.name} ${skill.description}`) }))
115
+ .map((skill) => ({ skill, score: relevanceScore(task, `${skill.name} ${skill.description}`, skill.name) }))
116
116
  .filter((item) => item.score > 0)
117
117
  .sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name))
118
118
  .slice(0, maxSkills);
119
119
  const commandMatches = [...state.commands.values()]
120
- .map((command) => ({ command, score: relevanceScore(task, `${command.name} ${command.description} ${command.argv.join(" ")}`) }))
120
+ .map((command) => ({ command, score: relevanceScore(task, `${command.name} ${command.description} ${command.argv.join(" ")}`, command.name) }))
121
121
  .filter((item) => item.score > 0)
122
122
  .sort((a, b) => b.score - a.score || a.command.name.localeCompare(b.command.name))
123
123
  .slice(0, 20);
@@ -718,7 +718,7 @@ function capabilityFingerprint(state, skills) {
718
718
  }));
719
719
  }
720
720
 
721
- function relevanceScore(task, candidate) {
721
+ function relevanceScore(task, candidate, identity = "") {
722
722
  const taskTokens = tokenize(task);
723
723
  const candidateTokens = tokenize(candidate);
724
724
  if (!taskTokens.size || !candidateTokens.size) return 0;
@@ -726,18 +726,31 @@ function relevanceScore(task, candidate) {
726
726
  for (const token of taskTokens) {
727
727
  if (candidateTokens.has(token)) score += token.length >= 6 ? 2 : 1;
728
728
  }
729
- const taskLower = task.toLowerCase();
730
- const candidateLower = candidate.toLowerCase();
731
- if (candidateLower.includes(taskLower) || taskLower.includes(candidateLower)) score += 4;
729
+ const taskComparable = comparableText(task);
730
+ const candidateComparable = comparableText(candidate);
731
+ const identityComparable = comparableText(identity);
732
+ if (candidateComparable.includes(taskComparable) || taskComparable.includes(candidateComparable)) score += 4;
733
+ if (identityComparable.length >= 3 && ` ${taskComparable} `.includes(` ${identityComparable} `)) score += 12;
732
734
  return score;
733
735
  }
734
736
 
737
+ function comparableText(value) {
738
+ return String(value || "")
739
+ .toLowerCase()
740
+ .replace(/[^a-z0-9\p{Script=Han}]+/gu, " ")
741
+ .trim()
742
+ .replace(/\s+/g, " ");
743
+ }
744
+
735
745
  function tokenize(value) {
736
746
  const text = String(value || "").toLowerCase();
737
747
  const tokens = new Set();
738
748
  for (const raw of text.match(/[a-z0-9_][a-z0-9_.-]{1,}/g) || []) {
739
749
  const token = raw.replace(/^[.-]+|[.-]+$/g, "");
740
750
  if (token.length >= 2 && !TOKEN_STOP_WORDS.has(token)) tokens.add(token);
751
+ for (const part of token.split(/[._-]+/)) {
752
+ if (part.length >= 2 && !TOKEN_STOP_WORDS.has(part)) tokens.add(part);
753
+ }
741
754
  }
742
755
  for (const sequence of text.match(/[\p{Script=Han}]{1,}/gu) || []) {
743
756
  if (!TOKEN_STOP_WORDS.has(sequence)) tokens.add(sequence);
@@ -109,6 +109,7 @@ export class AppAutomationManager {
109
109
  maxDepth: clampInt(args.max_depth, 6, 1, MAX_UI_DEPTH),
110
110
  maxElements: clampInt(args.max_elements, 200, 1, MAX_UI_ELEMENTS),
111
111
  includeValues: args.include_values === true,
112
+ includeMenus: args.include_menus === true,
112
113
  };
113
114
  const result = await this.runJxa(payload, clampInt(args.timeout_seconds, 30, 1, 120), context);
114
115
  return {
@@ -143,6 +144,7 @@ export class AppAutomationManager {
143
144
  value,
144
145
  maxDepth: clampInt(args.max_depth, 8, 1, MAX_UI_DEPTH),
145
146
  maxElements: MAX_UI_ELEMENTS,
147
+ includeMenus: args.include_menus === true,
146
148
  };
147
149
  const result = await this.runJxa(payload, clampInt(args.timeout_seconds, 30, 1, 120), context);
148
150
  return {
@@ -168,9 +170,11 @@ export class AppAutomationManager {
168
170
  undefined,
169
171
  input,
170
172
  );
173
+ const output = result.stdout.trim();
174
+ if (!output) throw new Error("macOS accessibility helper returned no JSON output");
171
175
  let parsed;
172
176
  try {
173
- parsed = JSON.parse(result.stdout.trim() || "{}");
177
+ parsed = JSON.parse(output);
174
178
  } catch {
175
179
  throw new Error("macOS accessibility helper returned invalid JSON");
176
180
  }
@@ -337,7 +341,7 @@ function describe(element, index, includeValues) {
337
341
  return item;
338
342
  }
339
343
  function childrenOf(element) { return safe(() => element.uiElements(), []); }
340
- function flatten(root, maxDepth, maxElements, includeValues) {
344
+ function flatten(root, maxDepth, maxElements, includeValues, includeMenus) {
341
345
  const output = [];
342
346
  const elements = [];
343
347
  const stack = [{ element: root, depth: 0 }];
@@ -346,10 +350,12 @@ function flatten(root, maxDepth, maxElements, includeValues) {
346
350
  const children = childrenOf(current.element);
347
351
  for (let i = children.length - 1; i >= 0; i--) {
348
352
  const child = children[i];
353
+ const item = describe(child, output.length, includeValues);
349
354
  elements.push(child);
350
- output.push(describe(child, output.length, includeValues));
355
+ output.push(item);
351
356
  if (output.length >= maxElements) break;
352
- if (current.depth + 1 < maxDepth) stack.push({ element: child, depth: current.depth + 1 });
357
+ const isMenuTree = String(item.role || '').startsWith('AXMenu');
358
+ if (current.depth + 1 < maxDepth && (includeMenus || !isMenuTree)) stack.push({ element: child, depth: current.depth + 1 });
353
359
  }
354
360
  }
355
361
  return { output, elements, truncated: stack.length > 0 || output.length >= maxElements };
@@ -366,14 +372,14 @@ function main() {
366
372
  const process = se.applicationProcesses.byName(payload.application);
367
373
  if (!safe(() => process.exists(), false)) throw new Error('application process not found or Accessibility access denied');
368
374
  if (payload.operation === 'inspect') {
369
- const flattened = flatten(process, payload.maxDepth, payload.maxElements, payload.includeValues === true);
370
- return { frontmost: safe(() => process.frontmost(), false), elements: flattened.output, truncated: flattened.truncated };
375
+ const flattened = flatten(process, payload.maxDepth, payload.maxElements, payload.includeValues === true, payload.includeMenus === true);
376
+ return { frontmost: safe(() => process.frontmost(), false), elements: flattened.output, truncated: flattened.truncated, menus_included: payload.includeMenus === true };
371
377
  }
372
378
  if (payload.action === 'activate') {
373
379
  process.frontmost = true;
374
380
  return { ok: true, matched: 1 };
375
381
  }
376
- const flattened = flatten(process, payload.maxDepth, payload.maxElements, true);
382
+ const flattened = flatten(process, payload.maxDepth, payload.maxElements, true, payload.includeMenus === true);
377
383
  const matchesList = [];
378
384
  for (let i = 0; i < flattened.output.length; i++) if (matches(flattened.output[i], payload.selector)) matchesList.push(i);
379
385
  const chosen = payload.selector.index !== undefined ? matchesList[payload.selector.index] : matchesList[0];
@@ -398,5 +404,5 @@ function main() {
398
404
  }
399
405
  return { ok: true, matched: matchesList.length, selected_index: chosen, element: describe(element, chosen, false) };
400
406
  }
401
- try { console.log(JSON.stringify(main())); } catch (error) { console.log(JSON.stringify({ error: String(error.message || error) })); }
407
+ (() => { try { return JSON.stringify(main()); } catch (error) { return JSON.stringify({ error: String(error.message || error) }); } })()
402
408
  `;
@@ -200,6 +200,11 @@
200
200
  "type": "boolean",
201
201
  "default": false
202
202
  },
203
+ "include_menus": {
204
+ "type": "boolean",
205
+ "default": false,
206
+ "description": "Include and recursively inspect the application menu hierarchy. Disabled by default so main-window controls are reached quickly."
207
+ },
203
208
  "timeout_seconds": {
204
209
  "type": "integer",
205
210
  "minimum": 1,
@@ -286,6 +291,11 @@
286
291
  "type": "string",
287
292
  "pattern": "^[a-z][a-z0-9._-]{0,63}$"
288
293
  },
294
+ "include_menus": {
295
+ "type": "boolean",
296
+ "default": false,
297
+ "description": "Include menu-bar and menu subtrees when matching the action selector."
298
+ },
289
299
  "max_depth": {
290
300
  "type": "integer",
291
301
  "minimum": 1,
@@ -3,7 +3,7 @@ import toolCatalog from "../shared/tool-catalog.json";
3
3
  import serverMetadata from "../shared/server-metadata.json";
4
4
 
5
5
  const SERVER_NAME = String(serverMetadata.name);
6
- const SERVER_VERSION = "0.12.0";
6
+ const SERVER_VERSION = "0.12.1";
7
7
  const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
8
8
  const MCP_SUPPORTED_PROTOCOL_VERSIONS = serverMetadata.supportedProtocolVersions.map((value) => String(value));
9
9
  const JSONRPC_VERSION = "2.0";