@remcp/runtime 0.2.129 → 0.2.130

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.130 — 2026-10-02
4
+
5
+ - `computer_snapshot` now skips desktop-only probes on headless Linux and returns stable empty/unavailable desktop fields without false error noise. The shared `desktopSessionAvailable()` helper keeps this behavior aligned with live capability discovery.
6
+ - Linux `screenshot_region` advertising now uses the same backend resolver as execution: `grim` is Wayland-only, portal capture requires a Wayland portal candidate plus `ffmpeg`, GNOME capture requires `gnome-screenshot` + `ffmpeg`, and X11 uses ImageMagick `import`.
7
+ - Wayland + `ffmpeg` alone no longer exposes a screenshot tool that can only fail at runtime. Tool names and MCP schemas remain unchanged.
8
+
3
9
  ## 0.2.129 — 2026-10-02
4
10
 
5
11
  - Linux Desktop capability discovery now requires an active `DISPLAY` or `WAYLAND_DISPLAY`, preventing headless hosts from advertising desktop actions just because helper binaries are installed.
package/README.md CHANGED
@@ -80,7 +80,10 @@ agent forwards the refreshed tool names to the hosted relay. Browser/CDP tools a
80
80
  example: they are enabled only while the runtime can use a supported loopback browser endpoint. On Linux,
81
81
  Desktop capabilities additionally require an active graphical session (`DISPLAY` or `WAYLAND_DISPLAY`):
82
82
  headless hosts do not advertise window/UI/input/clipboard/display/screenshot/launch/open/reveal/notification
83
- operations merely because desktop helper binaries happen to be installed.
83
+ operations merely because desktop helper binaries happen to be installed. The universal `computer_snapshot`
84
+ remains available on those hosts but skips unavailable desktop probes instead of reporting synthetic backend
85
+ errors. On Linux, `screenshot_region` is advertised only when the current session has a real matching capture
86
+ backend (Wayland grim/GNOME/portal path or X11 ImageMagick), not merely because `ffmpeg` is installed.
84
87
 
85
88
  The generated hosted reference covers the current **10 hosted production definitions**, all model-visible
86
89
  and routing to the 83 granular runtime operations described above; the optional widget-enabled surface is 15.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/runtime",
3
- "version": "0.2.129",
3
+ "version": "0.2.130",
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",
@@ -7,8 +7,9 @@ const JSON_OUTPUT_SCHEMA = {
7
7
  import { browserControlAvailable, browserHandlers, browserRemoteEnabled } from './browser.mjs';
8
8
  import { isWaylandSession } from '../screenshot-portal.mjs';
9
9
  import { waylandPortalCandidate } from '../wayland-remote-desktop.mjs';
10
- import { commandExists, runFile } from './common.mjs';
10
+ import { commandExists, desktopSessionAvailable, runFile } from './common.mjs';
11
11
  import { desktopHandlers } from './desktop.mjs';
12
+ import { regionScreenshotBackends } from './desktop-linux.mjs';
12
13
  import { diagnosticHandlers, gnomeScreencastSupported, recordScreenAvailable } from './diagnostics.mjs';
13
14
  import { documentHandlers } from './documents.mjs';
14
15
 
@@ -604,9 +605,7 @@ export function desktopShellCapabilitySnapshot({
604
605
  commandExistsFn = commandExists,
605
606
  } = {}) {
606
607
  const nativeDesktop = platform === 'win32' || platform === 'darwin';
607
- const linuxDesktop = platform === 'linux'
608
- && Boolean(String(env?.DISPLAY || '').trim() || String(env?.WAYLAND_DISPLAY || '').trim());
609
- const desktopSession = nativeDesktop || linuxDesktop;
608
+ const desktopSession = desktopSessionAvailable({ platform, env });
610
609
  return {
611
610
  desktop_session:desktopSession,
612
611
  desktop_open:desktopSession && (nativeDesktop || commandExistsFn('xdg-open')),
@@ -646,7 +645,8 @@ export async function capabilitySnapshot() {
646
645
  const pyAtSpi = linux && desktopSession ? await linuxHasPyAtSpi() : false;
647
646
  const wayland = linux && desktopSession && isWaylandSession();
648
647
  const gnomeRecord = wayland ? await gnomeScreencastSupported() : false;
649
- const portalInput = wayland && waylandPortalCandidate();
648
+ const portalCandidate = wayland && waylandPortalCandidate();
649
+ const portalInput = portalCandidate;
650
650
  const x11Input = linux && desktopSession && !wayland && (commandExists('wdotool') || commandExists('xdotool'));
651
651
  const browserCdp = browserRemoteEnabled() && await browserControlAvailable(undefined, 500);
652
652
  const documentCapabilities = documentCapabilitySnapshot();
@@ -659,7 +659,7 @@ export async function capabilitySnapshot() {
659
659
  drag: win || mac || (desktopSession && (portalInput || x11Input)),
660
660
  clipboard: win || mac || (desktopSession && (commandExists('wl-paste') || commandExists('xclip') || commandExists('xsel'))),
661
661
  displays: win || mac || (desktopSession && ((wayland && linux) || commandExists('wlr-randr') || commandExists('xrandr'))),
662
- screen_region: win || mac || (desktopSession && (commandExists('grim') || commandExists('import') || (isWaylandSession() && commandExists('ffmpeg')))),
662
+ screen_region: win || mac || (desktopSession && regionScreenshotBackends({ wayland, portalCandidate }).length > 0),
663
663
  notifications: win || mac || (desktopSession && commandExists('notify-send')),
664
664
  browser_cdp: browserCdp,
665
665
  browser_evaluate: browserCdp && (process.env.NODE_ENV !== 'production' || ['1', 'true', 'yes', 'on'].includes(String(process.env.REMCP_BROWSER_ALLOW_EVALUATE || '').toLowerCase())),
@@ -21,6 +21,15 @@ export function optionalString(value) {
21
21
  return typeof value === 'string' && value.trim() ? value.trim() : null;
22
22
  }
23
23
 
24
+ export function desktopSessionAvailable({
25
+ platform = process.platform,
26
+ env = process.env,
27
+ } = {}) {
28
+ if (platform === 'win32' || platform === 'darwin') return true;
29
+ if (platform !== 'linux') return false;
30
+ return Boolean(String(env?.DISPLAY || '').trim() || String(env?.WAYLAND_DISPLAY || '').trim());
31
+ }
32
+
24
33
  export function requireEnum(value, field, allowed) {
25
34
  const normalized = String(value ?? '');
26
35
  if (!allowed.includes(normalized)) fail(`${field} must be one of: ${allowed.join(', ')}`);
@@ -1487,12 +1487,13 @@ export function regionScreenshotBackends({
1487
1487
  gnomeScreenshot = commandExists('gnome-screenshot'),
1488
1488
  ffmpeg = commandExists('ffmpeg'),
1489
1489
  imagemagick = commandExists('import'),
1490
+ portalCandidate = waylandPortalCandidate(),
1490
1491
  } = {}) {
1491
1492
  const backends = [];
1492
- if (grim) backends.push('grim');
1493
+ if (wayland && grim) backends.push('grim');
1493
1494
  if (wayland) {
1494
1495
  if (gnomeScreenshot && ffmpeg) backends.push('gnome-screenshot');
1495
- if (ffmpeg) backends.push('portal');
1496
+ if (ffmpeg && portalCandidate) backends.push('portal');
1496
1497
  } else if (imagemagick) {
1497
1498
  backends.push('imagemagick');
1498
1499
  }
@@ -4,7 +4,7 @@ import { fileToolHandlers } from '../tools/files.mjs';
4
4
  import { liveConfig, runtimeConfig } from '../config.mjs';
5
5
  import { assertAllowedCommand } from '../policy.mjs';
6
6
  import { multi, resolveSafePath, text } from '../util.mjs';
7
- import { clamp, jsonResult, optionalString, requireEnum } from './common.mjs';
7
+ import { clamp, desktopSessionAvailable, jsonResult, optionalString, requireEnum } from './common.mjs';
8
8
  import * as linux from './desktop-linux.mjs';
9
9
  import * as macos from './desktop-macos.mjs';
10
10
  import * as windows from './desktop-windows.mjs';
@@ -1026,12 +1026,14 @@ function computerScreenshotTask(args = {}) {
1026
1026
  }
1027
1027
 
1028
1028
  export async function computerSnapshot(args = {}) {
1029
- const captureScreenshot = args.include_screenshot !== false || args.include_ocr === true;
1029
+ const desktopAvailable = desktopSessionAvailable();
1030
+ const screenshotRequested = args.include_screenshot !== false || args.include_ocr === true;
1031
+ const captureScreenshot = desktopAvailable && screenshotRequested;
1030
1032
  const includeUi = args.include_ui !== false;
1031
1033
  const [windowsResult, displaysResult, uiResult, clipboardResult, cursorResult, shotResult] = await Promise.allSettled([
1032
- listWindows(),
1033
- displayInventory(),
1034
- includeUi
1034
+ desktopAvailable ? listWindows() : Promise.resolve(null),
1035
+ desktopAvailable ? displayInventory() : Promise.resolve(null),
1036
+ includeUi && desktopAvailable
1035
1037
  ? uiSnapshot({
1036
1038
  max_nodes:args.max_ui_nodes || 800,
1037
1039
  max_depth:args.max_ui_depth || 12,
@@ -1040,8 +1042,8 @@ export async function computerSnapshot(args = {}) {
1040
1042
  ...(args.ui_browser_dom === true ? { browser_dom:true } : {}),
1041
1043
  })
1042
1044
  : Promise.resolve(null),
1043
- clipboard({ action:'read' }),
1044
- cursorPosition(),
1045
+ desktopAvailable ? clipboard({ action:'read' }) : Promise.resolve(null),
1046
+ desktopAvailable ? cursorPosition() : Promise.resolve(null),
1045
1047
  captureScreenshot ? computerScreenshotTask(args) : Promise.resolve(null),
1046
1048
  ]);
1047
1049
  const parse = (result, fallback = null) => {
@@ -1069,7 +1071,7 @@ export async function computerSnapshot(args = {}) {
1069
1071
  if (includeUi && parsedUi !== ui && parsedUi != null) {
1070
1072
  shapeErrors.push({ source:'ui', message:'accessibility snapshot returned an invalid result shape' });
1071
1073
  }
1072
- const clipboardOk = clipboardResult.status === 'fulfilled' && clipboardResult.value?.isError !== true;
1074
+ const clipboardOk = desktopAvailable && clipboardResult.status === 'fulfilled' && clipboardResult.value?.isError !== true && Boolean(clipboardResult.value);
1073
1075
  const clipText = clipboardOk ? clipboardResult.value.content?.[0]?.text || '' : '';
1074
1076
  const uiNodes = Array.isArray(ui?.nodes) ? ui.nodes : Array.isArray(ui) ? ui : [];
1075
1077
  const focusedNodes = uiNodes.filter(node => node.focused === true);
@@ -1152,8 +1154,9 @@ export async function computerSnapshot(args = {}) {
1152
1154
  }
1153
1155
  }
1154
1156
 
1155
- const shouldOcr = args.include_ocr === true
1156
- || (args.include_ocr !== false && captureScreenshot && uiNodes.length === 0 && !browser);
1157
+ const ocrRequested = args.include_ocr === true
1158
+ || (desktopAvailable && args.include_ocr !== false && screenshotRequested && uiNodes.length === 0 && !browser);
1159
+ const shouldOcr = desktopAvailable && ocrRequested;
1157
1160
  let ocr = null;
1158
1161
  let ocrCaptureBounds = null;
1159
1162
  let ocrShotResult = shotResult;
@@ -1193,14 +1196,14 @@ export async function computerSnapshot(args = {}) {
1193
1196
  ...(browser ? { browser } : {}),
1194
1197
  ...(ocr ? { ocr } : {}),
1195
1198
  fallback_chain: {
1196
- accessibility: { requested:includeUi, available:includeUi && uiResult.status === 'fulfilled', nodes:uiNodes.length },
1199
+ accessibility: { requested:includeUi, available:includeUi && desktopAvailable && uiResult.status === 'fulfilled' && Boolean(uiResult.value), nodes:uiNodes.length },
1197
1200
  native_browser_dom: {
1198
1201
  requested:includeUi && args.ui_browser_dom === true,
1199
1202
  available:Boolean(includeUi && ui?.browser_dom?.available),
1200
1203
  provider:ui?.browser_dom?.provider || null,
1201
1204
  },
1202
1205
  browser_dom: { requested:browserRequested, available:Boolean(browser) },
1203
- ocr: { requested:shouldOcr, available:Boolean(ocr?.available), backend:ocr?.backend || null },
1206
+ ocr: { requested:ocrRequested, available:Boolean(ocr?.available), backend:ocr?.backend || null },
1204
1207
  vision_screenshot: { available:shotResult.status === 'fulfilled' && Boolean(shotResult.value?.content?.some(part => part.type === 'image')) },
1205
1208
  },
1206
1209
  visual: {