@remcp/runtime 0.2.75 → 0.2.77

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
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.2.77 — 2026-09-21
6
+
7
+ - Fixed `browser_action` typing so the documented `text` argument reaches CDP `Input.insertText` with Unicode intact; `value` and `text_value` remain compatible aliases.
8
+ - Clarified the browser action schema so agents use `text` for typing and reusable selectors from `browser_find`.
9
+ - Lockstep patch release with the ReMCP client/server at `0.2.77`.
10
+
11
+ ## 0.2.76 — 2026-09-21
12
+
13
+ - `browser_find(text=...)` ranks exact semantic targets ahead of broad ancestor text containers, prefers actionable/semantic nodes, and de-duplicates nested matches before applying the result limit.
14
+ - Native `h1`–`h6` elements derive the `heading` role, improving browser text targeting and follow-up actions.
15
+ - Lockstep patch release with the ReMCP client/server at `0.2.76`.
16
+
5
17
  ## 0.2.63 — 2026-09-20
6
18
 
7
19
  - Lockstep MCP Apps production-surface release with the ReMCP client/server at `0.2.63`; device runtime tool behavior is unchanged from `0.2.62`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/runtime",
3
- "version": "0.2.75",
3
+ "version": "0.2.77",
4
4
  "description": "First-party ReMCP local device runtime: filesystem, terminal, computer-use, browser, diagnostics and document tools over MCP.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -447,7 +447,7 @@ export async function browserSnapshot(args) {
447
447
  });
448
448
  }
449
449
 
450
- function findExpression(args) {
450
+ export function findExpression(args) {
451
451
  const selector = optionalString(args.selector);
452
452
  const needle = optionalString(args.text);
453
453
  const role = optionalString(args.role);
@@ -474,6 +474,7 @@ function findExpression(args) {
474
474
  const explicit = (el.getAttribute('role') || '').trim().toLowerCase();
475
475
  if (explicit) return explicit;
476
476
  const tag = el.tagName.toLowerCase();
477
+ if (/^h[1-6]$/.test(tag)) return 'heading';
477
478
  if (tag === 'button') return 'button';
478
479
  if (tag === 'a' && el.hasAttribute('href')) return 'link';
479
480
  if (tag === 'textarea') return 'textbox';
@@ -495,15 +496,57 @@ function findExpression(args) {
495
496
  }
496
497
  return '';
497
498
  }
498
- return source.filter(el => {
499
+ function visibleText(el) {
500
+ return (el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim();
501
+ }
502
+ function textQuality(value) {
503
+ if (!needle) return 0;
504
+ const hay = value.toLowerCase();
505
+ const wanted = needle.toLowerCase();
506
+ if (hay === wanted) return 0;
507
+ if (hay.startsWith(wanted)) return 1;
508
+ return 2;
509
+ }
510
+ function depthOf(el) {
511
+ let depth = 0;
512
+ for (let current = el.parentElement; current; current = current.parentElement) depth += 1;
513
+ return depth;
514
+ }
515
+ const candidates = source.filter(el => {
499
516
  const style = getComputedStyle(el);
500
517
  const visible = style.visibility !== 'hidden' && style.display !== 'none' && el.getClientRects().length > 0;
501
- const hay = (el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim();
518
+ const hay = visibleText(el);
502
519
  const r = semanticRole(el);
503
520
  return visible && (!needle || hay.toLowerCase().includes(needle.toLowerCase())) && (!role || r === role.toLowerCase());
504
- }).slice(0, ${limit}).map(el => {
521
+ }).map((el, index) => ({
522
+ el,
523
+ index,
524
+ text: visibleText(el),
525
+ role: semanticRole(el),
526
+ depth: depthOf(el),
527
+ }));
528
+ let selected = candidates;
529
+ if (needle && !selector) {
530
+ selected = [...candidates].sort((a, b) =>
531
+ textQuality(a.text) - textQuality(b.text)
532
+ || Number(!a.role) - Number(!b.role)
533
+ || a.text.length - b.text.length
534
+ || b.depth - a.depth
535
+ || a.index - b.index
536
+ );
537
+ const deduped = [];
538
+ for (const candidate of selected) {
539
+ if (deduped.some(existing => existing.el.contains(candidate.el) || candidate.el.contains(existing.el))) continue;
540
+ deduped.push(candidate);
541
+ if (deduped.length >= ${limit}) break;
542
+ }
543
+ selected = deduped;
544
+ } else {
545
+ selected = selected.slice(0, ${limit});
546
+ }
547
+ return selected.map(({ el }) => {
505
548
  const b = el.getBoundingClientRect();
506
- return { selector: cssPath(el), tag: el.tagName.toLowerCase(), role: semanticRole(el), name: el.getAttribute('aria-label') || el.getAttribute('name') || '', text: (el.innerText || el.textContent || '').trim().slice(0, 500), value: 'value' in el ? String(el.value).slice(0, 500) : '', x: b.x, y: b.y, width: b.width, height: b.height, disabled: Boolean(el.disabled) };
549
+ return { selector: cssPath(el), tag: el.tagName.toLowerCase(), role: semanticRole(el), name: el.getAttribute('aria-label') || el.getAttribute('name') || '', text: visibleText(el).slice(0, 500), value: 'value' in el ? String(el.value).slice(0, 500) : '', x: b.x, y: b.y, width: b.width, height: b.height, disabled: Boolean(el.disabled) };
507
550
  });
508
551
  })()`;
509
552
  }
@@ -529,6 +572,12 @@ function elementLookup(args) {
529
572
  })()`;
530
573
  }
531
574
 
575
+ export function browserActionInputValue(args = {}, action = '') {
576
+ // text is the public field models naturally use for type, while set_value/select historically
577
+ // used value/text_value. Keep all aliases compatible but give each action its semantic field.
578
+ return String(action === 'type' ? (args.text ?? args.text_value ?? args.value ?? '') : (args.value ?? args.text_value ?? args.text ?? ''));
579
+ }
580
+
532
581
  export async function browserAction(args) {
533
582
  const action = requireEnum(args.action, 'action', ['click', 'focus', 'type', 'set_value', 'select', 'scroll_into_view', 'upload', 'press', 'set_viewport']);
534
583
  return withTarget(args, async (session, target) => {
@@ -568,7 +617,7 @@ export async function browserAction(args) {
568
617
  return jsonResult({ target_id: target.id, action, key });
569
618
  }
570
619
  const lookup = elementLookup(args);
571
- const value = String(args.value ?? args.text_value ?? '');
620
+ const value = browserActionInputValue(args, action);
572
621
  const option = String(args.option ?? value);
573
622
 
574
623
  if (action === 'click') {
@@ -555,7 +555,7 @@ export const extendedToolDefinitions = [
555
555
  define('browser_navigate', 'Browser navigate', 'Open a new debuggable tab or navigate a selected browser page by URL, history back/forward, or reload through Chrome DevTools Protocol.', o({ ...browserTarget, action:e(['url','new_tab','back','forward','reload'], 'Default url when url is provided, otherwise reload. new_tab creates the first page target when needed.'), url:s('Destination URL for action=url or action=new_tab.'), wait:b('Wait for the page load event; default true.'), ignore_cache:b('Reload without cache when action=reload.') }), openMutatingNonDestructive, browserHandlers.browser_navigate, ['browser_cdp']),
556
556
  define('browser_snapshot', 'Browser snapshot', 'Return the selected browser page accessibility tree through Chrome DevTools Protocol and, when requested, attach a real rendered viewport PNG for visual verification. A selector is temporarily recentered for capture, its exact viewport bounds are returned, and the prior scroll position is restored before return.', o({ ...browserTarget, max_nodes:n('Maximum AX nodes; default 1500.'), include_screenshot:b('Attach a real CDP-rendered viewport PNG screenshot; default false.'), selector:s('When include_screenshot=true, center this CSS-selected element before capture and report its viewport bounds.') }), readOnlyLive, browserHandlers.browser_snapshot, ['browser_cdp']),
557
557
  define('browser_find', 'Find browser element', 'Find visible DOM elements by CSS selector, text, or ARIA role and return reusable selectors plus text, value, and bounding boxes.', o({ ...browserTarget, selector:s('CSS selector.'), text:s('Visible text substring.'), role:s('ARIA role.'), limit:n() }), readOnlyLive, browserHandlers.browser_find, ['browser_cdp']),
558
- define('browser_action', 'Browser element action', 'Click, focus, type/set a value, select, scroll to, upload files, press a key, or set an exact responsive-test viewport in a debuggable browser page using DOM/CDP semantics.', o({ ...browserTarget, action:e(['click','focus','type','set_value','select','scroll_into_view','upload','press','set_viewport']), selector:s(), text:s(), value:s(), text_value:s(), option:s(), path:s(), paths:{type:'array',items:{type:'string'}}, key:s(), width:n('CSS viewport width for set_viewport.'), height:n('CSS viewport height for set_viewport.'), device_scale_factor:n('Device scale factor for set_viewport; default 1.'), mobile:b('Enable mobile emulation for set_viewport.') }, ['action']), openMutating, browserHandlers.browser_action, ['browser_cdp']),
558
+ define('browser_action', 'Browser element action', 'Click, focus, type/set a value, select, scroll to, upload files, press a key, or set an exact responsive-test viewport in a debuggable browser page using DOM/CDP semantics.', o({ ...browserTarget, action:e(['click','focus','type','set_value','select','scroll_into_view','upload','press','set_viewport']), selector:s('CSS selector for the target element; prefer the reusable selector returned by browser_find.'), text:s('For action=type, Unicode text to insert. For element actions without selector, visible text may identify the target.'), value:s('Value for action=set_value; also accepted as a compatibility fallback for action=type.'), text_value:s('Compatibility alias for the type/set_value text value.'), option:s(), path:s(), paths:{type:'array',items:{type:'string'}}, key:s(), width:n('CSS viewport width for set_viewport.'), height:n('CSS viewport height for set_viewport.'), device_scale_factor:n('Device scale factor for set_viewport; default 1.'), mobile:b('Enable mobile emulation for set_viewport.') }, ['action']), openMutating, browserHandlers.browser_action, ['browser_cdp']),
559
559
  define('browser_wait', 'Wait for browser', 'Wait for DOM state, page text, URL, completed loading, a navigation away from the current URL, or a short network-idle period in a debuggable browser page. Use browser_evaluate separately for JavaScript predicates or page-specific inspection.', o({ ...browserTarget, condition:e(['selector','text','url_contains','load','navigation','network_idle']), selector:s(), text:s(), value:s(), poll_ms:n(), idle_ms:n('Required zero-in-flight network quiet window for network_idle; default 500 ms.') }), readOnlyLive, browserHandlers.browser_wait, ['browser_cdp']),
560
560
  define('browser_evaluate', 'Evaluate browser JavaScript', 'Evaluate JavaScript in a selected debuggable browser page and return its serializable value. This is a powerful escape hatch for page-specific automation.', o({ ...browserTarget, expression:s('JavaScript expression.'), await_promise:b('Await a returned Promise; default true.') }, ['expression']), openMutating, browserHandlers.browser_evaluate, ['browser_cdp']),
561
561