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 +13 -0
- package/browser-extension/manifest.json +2 -2
- package/docs/AGENT_CONTEXT.md +1 -1
- package/docs/LOCAL_AUTOMATION.md +3 -1
- package/docs/TESTING.md +12 -0
- package/package.json +3 -2
- package/src/local/agent-context.mjs +19 -6
- package/src/local/app-automation.mjs +14 -8
- package/src/shared/tool-catalog.json +10 -0
- package/src/worker/index.ts +1 -1
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.
|
|
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.
|
|
32
|
+
"version_name": "0.12.1"
|
|
33
33
|
}
|
package/docs/AGENT_CONTEXT.md
CHANGED
|
@@ -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
|
|
package/docs/LOCAL_AUTOMATION.md
CHANGED
|
@@ -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.
|
|
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
|
|
730
|
-
const
|
|
731
|
-
|
|
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(
|
|
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(
|
|
355
|
+
output.push(item);
|
|
351
356
|
if (output.length >= maxElements) break;
|
|
352
|
-
|
|
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 {
|
|
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,
|
package/src/worker/index.ts
CHANGED
|
@@ -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.
|
|
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";
|