@remcp/runtime 0.2.174 → 0.2.176

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,10 +1,17 @@
1
1
  # Changelog
2
2
 
3
- ## 0.2.174 — 2026-10-22
3
+ ## 0.2.176 — 2026-10-23
4
4
 
5
- - Preserve the 83 runtime operations and existing schemas while separating guarded browser CDP security policy and diagnostics recording backends.
6
- - Retain browser-origin, DNS/SSRF checks, platform protections and output limits without widening device permissions.
7
- - Publish alongside the refactored client/host at the same version for package compatibility.
5
+ - Reduce screenshot CDP round-trips by batching viewport resolution with capture and reusing measured element dimensions.
6
+ - Cache Accessibility enablement per WebSocket connection with single-flight behavior.
7
+ - Filter malformed, mismatched or unsafe page WebSocket targets during discovery rather than exposing unusable tabs.
8
+ - Keep browser URL/DNS validation, permissions and the existing 83 operation definitions unchanged.
9
+
10
+ ## 0.2.175 — 2026-10-22
11
+
12
+ - Carry forward the public 0.2.174 browser-policy and diagnostics-recording module extraction without weakening CDP or recording controls.
13
+ - Require the patched MCP client 2.3.1 development dependency and retain 83 runtime operation contracts.
14
+ - No new remote execution permissions or OAuth bypasses are introduced.
8
15
 
9
16
  ## 0.2.173 — 2026-10-22
10
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/runtime",
3
- "version": "0.2.174",
3
+ "version": "0.2.176",
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",
@@ -28,7 +28,7 @@
28
28
  "@modelcontextprotocol/server": "^2.3.0"
29
29
  },
30
30
  "devDependencies": {
31
- "@modelcontextprotocol/client": "^2.0.0"
31
+ "@modelcontextprotocol/client": "^2.3.1"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",
@@ -173,12 +173,18 @@ export async function browserCapabilityAvailable(endpoint, timeoutMs = 500) {
173
173
  // Validating every tab sequentially can require hundreds of serial DNS lookups,
174
174
  // making browser_tabs exceed the relay's deadline on machines with many tabs.
175
175
  // Keep DNS and security validation, but bound both the work and its concurrency.
176
- export async function filterBrowserTargets(list, { validate = assertResolvedPageUrl, targetId = null } = {}) {
176
+ export async function filterBrowserTargets(list, { validate = assertResolvedPageUrl, targetId = null, endpoint = null } = {}) {
177
177
  if (!Array.isArray(list)) return [];
178
178
  const candidates = [];
179
179
  for (const item of list) {
180
180
  if (!item || item.type !== 'page' || !item.webSocketDebuggerUrl) continue;
181
181
  if (targetId && item.id !== targetId) continue;
182
+ // Reject fabricated/stale CDP socket metadata *before* DNS validation.
183
+ // browser_tabs must never advertise a target that cannot pass chooseTarget.
184
+ if (endpoint) {
185
+ try { validatedTargetWebSocketUrl(item, endpoint); }
186
+ catch { continue; }
187
+ }
182
188
  candidates.push(item);
183
189
  if (candidates.length >= 1000) break;
184
190
  }
@@ -242,13 +248,18 @@ export async function listBrowserTargets(endpoint, { targetId = null, readOnlySi
242
248
  throw new Error(`${label} is unavailable at ${base.origin}. Read-only browser_tabs and browser_snapshot do not launch a browser. Use ui_snapshot and ui_find for the existing browser window; do not retry this CDP endpoint unchanged. Only browser_navigate(action=new_tab), when explicitly requested, may start a local isolated browser.${browserLaunchExitHint({ configured })}`);
243
249
  }
244
250
  // Retain the current bounded concurrent DNS/security checks for up to 1000 tabs.
245
- return filterBrowserTargets(list, { targetId, validate });
251
+ return filterBrowserTargets(list, { targetId, validate, endpoint:base });
246
252
  }
247
253
 
248
254
  export function validatedTargetWebSocketUrl(target, endpoint) {
249
255
  let socketUrl;
250
256
  try { socketUrl = new URL(String(target?.webSocketDebuggerUrl)); }
251
257
  catch { throw new Error('Browser returned an invalid CDP WebSocket URL'); }
258
+ const targetId = String(target?.id ?? '');
259
+ if (!targetId || targetId.length > 256 || socketUrl.username || socketUrl.password ||
260
+ socketUrl.search || socketUrl.hash) {
261
+ throw new Error('Browser returned an invalid CDP WebSocket target');
262
+ }
252
263
  if (!['ws:', 'wss:'].includes(socketUrl.protocol) || !isLoopbackHost(socketUrl.hostname)) {
253
264
  throw new Error('Browser CDP WebSocket target must be loopback-only');
254
265
  }
@@ -139,9 +139,42 @@ export function sensitiveAccessibilityDescendants(nodes) {
139
139
  return secret;
140
140
  }
141
141
 
142
+ // One viewport evaluation is enough for both dimensions, and may run while
143
+ // Chrome captures the screenshot. Selector capture already returned the viewport
144
+ // dimensions with the element bounds, so it needs no further CDP evaluation.
145
+ export async function captureViewportScreenshot(session, screenshotTarget = null) {
146
+ const dimensions = screenshotTarget
147
+ ? Promise.resolve({width:screenshotTarget.viewport_width, height:screenshotTarget.viewport_height})
148
+ : evaluate(session, '({width:innerWidth,height:innerHeight})').catch(() => null);
149
+ const image = session.send('Page.captureScreenshot', {
150
+ format:'png',fromSurface:true,captureBeyondViewport:false,
151
+ });
152
+ const [size,captured] = await Promise.all([dimensions,image]);
153
+ return {
154
+ captured,
155
+ viewport:{scope:'viewport',width:size?.width ?? null,height:size?.height ?? null},
156
+ };
157
+ }
158
+
159
+ // Enabling Accessibility is a per-CDP-socket state. Reuse the successful
160
+ // handshake for retained guarded sockets; a new socket always enables afresh.
161
+ const accessibilityReady = new WeakMap();
162
+ export async function ensureAccessibilityReady(session) {
163
+ let pending = accessibilityReady.get(session);
164
+ if (!pending) {
165
+ pending = Promise.resolve().then(() => session.send('Accessibility.enable'));
166
+ accessibilityReady.set(session,pending);
167
+ try { await pending; }
168
+ catch (error) {
169
+ if (accessibilityReady.get(session)===pending) accessibilityReady.delete(session);
170
+ throw error;
171
+ }
172
+ } else await pending;
173
+ }
174
+
142
175
  export async function browserSnapshot(args) {
143
176
  return withTarget(args, async (session, target) => {
144
- await session.send('Accessibility.enable');
177
+ await ensureAccessibilityReady(session);
145
178
  const { nodes = [] } = await session.send('Accessibility.getFullAXTree');
146
179
  const limit = clamp(args.max_nodes, 1500, 1, 10_000);
147
180
  const redacted = sensitiveAccessibilityDescendants(nodes);
@@ -206,27 +239,19 @@ export async function browserSnapshot(args) {
206
239
  const { original_scroll_x: _originalX, original_scroll_y: _originalY, ...reportedBounds } = bounds;
207
240
  payload.screenshot_target = { selector, ...reportedBounds };
208
241
  }
209
- payload.screenshot = {
210
- scope: 'viewport',
211
- width: await evaluate(session, 'innerWidth').catch(() => null),
212
- height: await evaluate(session, 'innerHeight').catch(() => null),
213
- };
214
- const result = jsonResult(payload);
215
- let captured;
242
+ let screenshot;
216
243
  try {
217
- captured = await session.send('Page.captureScreenshot', {
218
- format: 'png',
219
- fromSurface: true,
220
- captureBeyondViewport: false,
221
- });
244
+ screenshot = await captureViewportScreenshot(session, payload.screenshot_target);
222
245
  } finally {
223
246
  if (restoreScroll) {
224
247
  await evaluate(session, `window.scrollTo({left:${restoreScroll.x},top:${restoreScroll.y},behavior:'instant'})`).catch(() => {});
225
248
  }
226
249
  }
227
- const data = String(captured?.data || '');
250
+ const data = String(screenshot.captured?.data || '');
228
251
  if (!data) throw new Error('CDP returned no screenshot data');
229
252
  if (Buffer.byteLength(data, 'base64') > 8 * 1024 * 1024) throw new Error('Browser screenshot exceeds the 8 MiB inline image limit');
253
+ payload.screenshot = screenshot.viewport;
254
+ const result = jsonResult(payload);
230
255
  result.content.push({ type:'image', data, mimeType:'image/png' });
231
256
  return result;
232
257
  });