@remcp/runtime 0.2.236 → 0.2.238

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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.237 — 2026-11-06
4
+
5
+ - Detect Windows Session 0 without enumerating users or modifying Windows login/security settings, and report native desktop_session unavailable when no interactive process desktop exists.
6
+ - Hide native Windows GUI, capture and recording tools from advertised capability lists in Session 0 while retaining terminal, filesystem, document and independently available CDP/browser tools.
7
+ - Fail closed before invoking native desktop actions, screenshots and recording, including mixed action preflights.
8
+ - Cache only capability discovery, never action authorization; emit one atomic tools/list_changed notification when browser availability changes.
9
+ - Gate an experimental Linux WebRTC Stop acknowledgement on confirmed exit of its directly owned Python media worker, with bounded SIGTERM/SIGKILL escalation and fail-closed local admission. Do not equate this with signed process-tree or macOS/Windows native Stop proof; the feature remains off by default.
10
+ - Preserve runtime protocol compatibility and the published 0.2.235/0.2.236 fixes. Locked-user and background service session transitions require further native validation.
11
+
3
12
  ## 0.2.236 — 2026-11-05
4
13
 
5
14
  - Report CDP readiness only for eligible page targets and classify configured unavailable CDP connections with bounded diagnostics.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/runtime",
3
- "version": "0.2.236",
3
+ "version": "0.2.238",
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",
@@ -1,5 +1,6 @@
1
- import { TEXT_OUTPUT_SCHEMA, supportedCoreTools, toolDefinitions } from './catalog.mjs';
2
- import { advertisedExtendedTools, extendedToolDefinitions } from './extended/catalog.mjs';
1
+ import { TEXT_OUTPUT_SCHEMA, toolDefinitions } from './catalog.mjs';
2
+ import { extendedToolDefinitions } from './extended/catalog.mjs';
3
+ import { supportedRuntimeToolDefinitions } from './tool-availability.mjs';
3
4
 
4
5
  export const compactRuntimeGroups = Object.freeze({
5
6
  manage_files: Object.freeze([
@@ -126,11 +127,8 @@ function groupedDefinition(name, members) {
126
127
  }
127
128
 
128
129
  export async function supportedRuntimeOperationNames() {
129
- const supportedExtended = new Set((await advertisedExtendedTools()).map(tool => tool.name));
130
- return new Set([
131
- ...supportedCoreTools().map(tool => tool.name),
132
- ...extendedToolDefinitions.filter(tool => supportedExtended.has(tool.name)).map(tool => tool.name),
133
- ]);
130
+ const { core, extended } = await supportedRuntimeToolDefinitions();
131
+ return new Set([...core, ...extended].map(tool => tool.name));
134
132
  }
135
133
 
136
134
  export async function compactRuntimeToolDefinitions() {
@@ -11,6 +11,7 @@ import { isWaylandSession } from '../screenshot-portal.mjs';
11
11
  import { waylandPortalCandidate } from '../wayland-remote-desktop.mjs';
12
12
  import { commandExists, desktopSessionAvailable, runFile } from './common.mjs';
13
13
  import { desktopHandlers } from './desktop.mjs';
14
+ import { cachedWindowsDesktopSessionStatus } from './desktop-windows-session.mjs';
14
15
  import { displayInventoryAvailable, regionScreenshotBackends } from './desktop-linux.mjs';
15
16
  import { hydrateLinuxDesktopEnv } from './desktop-linux-session-env.mjs';
16
17
  import { diagnosticHandlers, gnomeScreencastSupported, recordScreenAvailable } from './diagnostics.mjs';
@@ -254,6 +255,24 @@ export function documentCapabilitySnapshot({
254
255
  };
255
256
  }
256
257
 
258
+ // A paired Windows service may be online in Session 0 while native desktop
259
+ // tools are impossible. Keep computer_snapshot and CDP discoverable; they
260
+ // provide their own unavailable status and/or browser-only operation.
261
+ export const WINDOWS_SESSION_SCOPED_CAPABILITIES = Object.freeze([
262
+ 'desktop_session', 'desktop_open', 'windows', 'ui', 'input', 'pointer',
263
+ 'drag', 'clipboard', 'displays', 'screen_region', 'notifications', 'record',
264
+ ]);
265
+
266
+ export function limitWindowsSessionCapabilities(capabilities, {
267
+ platform = process.platform,
268
+ sessionStatus = null,
269
+ } = {}) {
270
+ if (platform !== 'win32' || sessionStatus?.available === true) return capabilities;
271
+ const bounded = { ...capabilities };
272
+ for (const name of WINDOWS_SESSION_SCOPED_CAPABILITIES) bounded[name] = false;
273
+ return bounded;
274
+ }
275
+
257
276
  export async function capabilitySnapshot() {
258
277
  const win = process.platform === 'win32';
259
278
  const mac = process.platform === 'darwin';
@@ -270,7 +289,7 @@ export async function capabilitySnapshot() {
270
289
  const displayInventoryReady = linux && desktopSession ? await displayInventoryAvailable() : false;
271
290
  const browserCdp = browserRemoteEnabled() && await browserControlAvailable(undefined, 500);
272
291
  const documentCapabilities = documentCapabilitySnapshot();
273
- return {
292
+ const capabilities = {
274
293
  ...desktopShellCapabilities,
275
294
  webrtc_pilot:linux && desktopSession && webRtcWorkerAvailable(),
276
295
  windows: win || mac || (desktopSession && (commandExists('wdotool') || commandExists('wmctrl') || (wayland && pyAtSpi))),
@@ -293,6 +312,12 @@ export async function capabilitySnapshot() {
293
312
  record: recordScreenAvailable({ platform:process.platform, wayland, gnomeSupported:gnomeRecord }),
294
313
  ...documentCapabilities,
295
314
  };
315
+ return win
316
+ ? limitWindowsSessionCapabilities(capabilities, {
317
+ platform:process.platform,
318
+ sessionStatus:await cachedWindowsDesktopSessionStatus(),
319
+ })
320
+ : capabilities;
296
321
  }
297
322
 
298
323
  function isSupported(definition, capabilities) {
@@ -308,9 +333,9 @@ export function allExtendedTools() {
308
333
  return extendedToolDefinitions.map(publicShape);
309
334
  }
310
335
 
311
- export async function advertisedExtendedTools() {
312
- const capabilities = await capabilitySnapshot();
313
- return extendedToolDefinitions.filter(definition => isSupported(definition, capabilities)).map(publicShape);
336
+ export async function advertisedExtendedTools(capabilities = null) {
337
+ const current = capabilities ?? await capabilitySnapshot();
338
+ return extendedToolDefinitions.filter(definition => isSupported(definition, current)).map(publicShape);
314
339
  }
315
340
 
316
341
  export async function capabilityFingerprint() {
@@ -16,6 +16,7 @@ import { clipboard, displayInventory, keyboard, listWindows, notification, point
16
16
  import { dragDrop, launchApp, openPath, revealPath, typeText, uiFind, waitForUi } from './desktop-ui-actions.mjs';
17
17
  import { clickOcrText } from './desktop-vision.mjs';
18
18
  import { computerSnapshot } from './desktop-computer-snapshot.mjs';
19
+ import { assertWindowsInteractiveDesktop, guardWindowsDesktopTool } from './desktop-windows-session.mjs';
19
20
 
20
21
  export function browserComputerAction(args, action) {
21
22
  const mapped = {
@@ -368,3 +369,40 @@ export const desktopHandlers = {
368
369
  reveal_path: revealPath,
369
370
  notification,
370
371
  };
372
+
373
+ // Explicit browser-only actions and non-UI waits remain usable with no Windows
374
+ // desktop. Auto-targeted actions may need native input, so they fail closed.
375
+ // Mixed batches must not execute early browser steps before later unsafe native
376
+ // operations are rejected.
377
+ export function computerActionRequiresDesktop(args = {}) {
378
+ const action = String(args?.action || '');
379
+ const target = String(args?.target || args?.target_type || 'auto');
380
+ if (action === 'wait') return false;
381
+ if (action === 'batch') {
382
+ return !Array.isArray(args.steps) || args.steps.some(computerActionRequiresDesktop);
383
+ }
384
+ if (action === 'keyboard') {
385
+ return !(target === 'browser' && args.key && !args.shortcut && !Array.isArray(args.keys));
386
+ }
387
+ if (['ui','pointer','window','drag','clipboard','launch_app','open_path',
388
+ 'reveal_path','notification','multi_select','multi_edit','double_click',
389
+ 'right_click','move','down','up','minimize','maximize','restore',
390
+ 'move_resize','resize','close'].includes(action)) return true;
391
+ return target !== 'browser';
392
+ }
393
+
394
+ // Keep native desktop tools unavailable in Windows Session 0 without blocking
395
+ // terminal, filesystem and explicitly browser-targeted CDP actions.
396
+ const nativeDesktopTools = [
397
+ 'list_windows', 'window_action', 'launch_app', 'ui_snapshot', 'ui_find',
398
+ 'ui_action', 'type_text', 'keyboard', 'pointer', 'drag_drop', 'scroll',
399
+ 'wait_for_ui', 'clipboard', 'display_inventory', 'screenshot_region',
400
+ 'open_path', 'reveal_path', 'notification',
401
+ ];
402
+ for (const name of nativeDesktopTools) {
403
+ desktopHandlers[name] = guardWindowsDesktopTool(desktopHandlers[name]);
404
+ }
405
+ desktopHandlers.computer_action = async (args = {}) => {
406
+ if (computerActionRequiresDesktop(args)) await assertWindowsInteractiveDesktop();
407
+ return computerAction(args);
408
+ };
@@ -8,6 +8,7 @@ import { clamp, desktopSessionAvailable, jsonResult, optionalString, requireEnum
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';
11
+ import { windowsDesktopSessionStatus } from './desktop-windows-session.mjs';
11
12
  import { browserAction, browserCapabilityAvailable, browserSnapshot } from './browser.mjs';
12
13
  import { probeBrowserReadiness } from './browser-readiness.mjs';
13
14
  import { findOcrText, ocrImage } from './ocr.mjs';
@@ -43,8 +44,19 @@ export async function settleComputerSnapshotProbes(tasks, {
43
44
  return results;
44
45
  }
45
46
 
46
- export async function computerSnapshot(args = {}) {
47
- const desktopAvailable = desktopSessionAvailable();
47
+ export async function resolveComputerDesktopSession({
48
+ platform = process.platform,
49
+ available = desktopSessionAvailable(),
50
+ windowsProbe = windowsDesktopSessionStatus,
51
+ } = {}) {
52
+ if (!available) return { status:'unavailable', available:false, reason:'no_graphical_session' };
53
+ if (platform !== 'win32') return { status:'available', available:true };
54
+ return windowsProbe();
55
+ }
56
+
57
+ export async function computerSnapshot(args = {}, { desktopSessionProbe = resolveComputerDesktopSession } = {}) {
58
+ const desktopSession = await desktopSessionProbe();
59
+ const desktopAvailable = desktopSession.available !== false;
48
60
  const screenshotRequested = args.include_screenshot !== false || args.include_ocr === true;
49
61
  const captureScreenshot = desktopAvailable && screenshotRequested;
50
62
  const includeUi = args.include_ui !== false;
@@ -86,7 +98,7 @@ export async function computerSnapshot(args = {}) {
86
98
  shapeErrors.push({ source:'cursor', message:'cursor position returned an invalid result shape' });
87
99
  }
88
100
  const parsedUi = includeUi ? parse(uiResult, null) : null;
89
- const ui = objectRecord(parsedUi) || { platform:process.platform, count:0, nodes:[], ...(includeUi ? { unavailable:true } : { skipped:true }) };
101
+ const ui = objectRecord(parsedUi) || { platform:process.platform, count:0, nodes:[], ...(includeUi ? { unavailable:true, ...(desktopSession.available === false ? { reason:desktopSession.reason } : {}) } : { skipped:true }) };
90
102
  if (includeUi && parsedUi !== ui && parsedUi != null) {
91
103
  shapeErrors.push({ source:'ui', message:'accessibility snapshot returned an invalid result shape' });
92
104
  }
@@ -208,6 +220,7 @@ export async function computerSnapshot(args = {}) {
208
220
  device: { hostname: os.hostname(), platform: process.platform, arch: process.arch },
209
221
  platform: process.platform,
210
222
  hostname: os.hostname(),
223
+ desktop_session:desktopSession,
211
224
  active_app: activeApp,
212
225
  active_window: activeWindow,
213
226
  windows,
@@ -218,6 +231,7 @@ export async function computerSnapshot(args = {}) {
218
231
  ...(args.browser_readiness === true ? { browser_readiness:readiness || { status:'invalid_response', ready:false, candidate_pages:0, next_step:'use_native_accessibility' } } : {}),
219
232
  ...(ocr ? { ocr } : {}),
220
233
  fallback_chain: {
234
+ desktop_session:desktopSession,
221
235
  accessibility: { requested:includeUi, available:includeUi && desktopAvailable && uiResult.status === 'fulfilled' && Boolean(uiResult.value), nodes:uiNodes.length },
222
236
  native_browser_dom: {
223
237
  requested:includeUi && args.ui_browser_dom === true,
@@ -34,9 +34,16 @@ export function webRtcWorkerAvailable(env=process.env,platform=process.platform)
34
34
  export function createDesktopWebRtcBridge({
35
35
  env=process.env,platform=process.platform,spawnChild=spawn,
36
36
  now=Date.now,setIntervalFn=setInterval,clearIntervalFn=clearInterval,
37
+ stopExitTimeoutMs=5_000,terminationEscalationMs=3_000,
37
38
  }={}){
39
+ if(!Number.isSafeInteger(stopExitTimeoutMs)||stopExitTimeoutMs<100||
40
+ stopExitTimeoutMs>10_000||
41
+ !Number.isSafeInteger(terminationEscalationMs)||
42
+ terminationEscalationMs<100||terminationEscalationMs>10_000)
43
+ throw new RangeError('Invalid WebRTC native Stop exit deadline');
38
44
  let child=null, current=null, active=false, started=0, seen=0;
39
- let stdout='', sequence=0, clock=null;
45
+ let stdout='', sequence=0, clock=null, pendingExit=null;
46
+ let lastExitedSession=null;
40
47
  const waits=new Map();
41
48
 
42
49
  function rejectWaits(){
@@ -46,14 +53,45 @@ export function createDesktopWebRtcBridge({
46
53
  }
47
54
  waits.clear();
48
55
  }
49
- function shutdown(){
56
+ // A SIGTERM request or stdio Stop ACK is NOT evidence of process exit.
57
+ // Hold a local admission fence until the kernel reports the child exit.
58
+ // This covers the direct child only, not escaped descendants: production
59
+ // still requires separate verified process-tree containment.
60
+ function shutdown(alreadyExited=false){
50
61
  if(clock){clearIntervalFn(clock);clock=null;}
51
- const target=child;
62
+ const target=child,session=current;
52
63
  child=null;current=null;active=false;stdout='';
53
64
  rejectWaits();
54
- if(target&&!target.killed){
55
- try{target.stdin.end();target.kill('SIGTERM');}catch{}
65
+ if(!target)return pendingExit?.promise??Promise.resolve(false);
66
+ let confirmExit;
67
+ const exited=new Promise(resolve=>{confirmExit=resolve;});
68
+ const marker={target,promise:exited};
69
+ pendingExit=marker;
70
+ let escalation;
71
+ const onExit=()=>{
72
+ target.removeListener('exit',onExit);
73
+ if(escalation)clearTimeout(escalation);
74
+ if(pendingExit===marker)pendingExit=null;
75
+ lastExitedSession=session;
76
+ confirmExit(true);
77
+ };
78
+ target.once('exit',onExit);
79
+ if(alreadyExited||target.exitCode!==null&&target.exitCode!==undefined||
80
+ target.signalCode!==null&&target.signalCode!==undefined){
81
+ onExit();
82
+ }else{
83
+ // Only signal the child spawned by this bridge. SIGKILL is a bounded
84
+ // escalation, not a Stop proof: never release the fence without exit.
85
+ escalation=setTimeout(()=>{
86
+ if(pendingExit!==marker)return;
87
+ try{target.kill('SIGKILL');}catch{}
88
+ },terminationEscalationMs);
89
+ escalation.unref?.();
90
+ if(!target.killed){
91
+ try{target.stdin.end();target.kill('SIGTERM');}catch{}
92
+ }
56
93
  }
94
+ return exited;
57
95
  }
58
96
  function receive(chunk){
59
97
  stdout+=chunk.toString('utf8');
@@ -140,14 +178,16 @@ export function createDesktopWebRtcBridge({
140
178
  child=worker;stdout='';
141
179
  worker.stdout.on('data',chunk=>{if(child===worker)receive(chunk);});
142
180
  worker.once('error',()=>{if(child===worker)shutdown();});
143
- worker.once('exit',()=>{if(child===worker)shutdown();});
181
+ worker.once('exit',()=>{if(child===worker)shutdown(true);});
144
182
  }
145
183
 
146
184
  async function offer({session_id:sid,mode,sdp},extra={}){
147
185
  if(!UUID.test(String(sid))||mode!=='view'||typeof sdp!=='string'||
148
186
  Buffer.byteLength(sdp)>MAX_SDP||sdp.length<100)
149
187
  throw new Error('Invalid view-only WebRTC offer');
150
- if(current!==null||child)throw new Error('Another WebRTC session is active');
188
+ if(current!==null||child||pendingExit)
189
+ throw new Error('Another WebRTC session is active');
190
+ lastExitedSession=null;
151
191
  current=sid;started=seen=now();active=false;
152
192
  let cancelled=false;
153
193
  const cancel=()=>{cancelled=true;shutdown();};
@@ -189,10 +229,19 @@ export function createDesktopWebRtcBridge({
189
229
  try{
190
230
  const reply=await send('stop',sid,{},4000);
191
231
  if(reply.sessionId!==sid)throw new Error('Invalid WebRTC stop acknowledgement');
192
- return jsonResult({ok:true,sessionId:sid});
193
- }finally{shutdown();}
232
+ }catch(error){shutdown();throw error;}
233
+ const exited=shutdown();
234
+ let timer;
235
+ const confirmed=lastExitedSession===sid||await Promise.race([
236
+ exited,
237
+ new Promise(resolve=>{timer=setTimeout(()=>resolve(false),stopExitTimeoutMs);}),
238
+ ]).finally(()=>clearTimeout(timer));
239
+ if(!confirmed)
240
+ throw new Error('WebRTC worker Stop unconfirmed: native process still running');
241
+ return jsonResult({ok:true,sessionId:sid});
194
242
  }
195
- return {offer,heartbeat,stop,shutdown,
243
+ // Do not expose the internal alreadyExited override to callers.
244
+ return {offer,heartbeat,stop,shutdown:()=>shutdown(),
196
245
  get activeSession(){return current;},
197
246
  };
198
247
  }
@@ -0,0 +1,89 @@
1
+ // Windows native UI runs in a user's interactive session, never in Session 0.
2
+ // This only examines the current probe process; it does not enumerate users,
3
+ // request a login, open the secure desktop or change Windows permissions.
4
+ import { runPowerShell } from './common.mjs';
5
+
6
+ export const WINDOWS_DESKTOP_PROBE = String.raw`
7
+ $ErrorActionPreference='Stop'
8
+ $p=[System.Diagnostics.Process]::GetCurrentProcess()
9
+ [pscustomobject]@{
10
+ sessionId=$p.SessionId
11
+ userInteractive=[Environment]::UserInteractive
12
+ } | ConvertTo-Json -Compress
13
+ `;
14
+
15
+ export function classifyWindowsDesktopSession(value) {
16
+ const sessionId = value?.sessionId;
17
+ const interactive = value?.userInteractive;
18
+ if (!Number.isSafeInteger(sessionId) || sessionId < 0 || typeof interactive !== 'boolean') {
19
+ return { status:'unknown', available:null, reason:'invalid_session_probe' };
20
+ }
21
+ if (sessionId === 0 || !interactive) {
22
+ return {
23
+ status:'unavailable',
24
+ available:false,
25
+ reason:sessionId === 0 ? 'windows_session_0' : 'noninteractive_process',
26
+ next_step:'sign_in_to_interactive_desktop',
27
+ };
28
+ }
29
+ return { status:'available', available:true };
30
+ }
31
+
32
+ export async function windowsDesktopSessionStatus({ runPowerShellFn = runPowerShell } = {}) {
33
+ try {
34
+ const result = await runPowerShellFn(WINDOWS_DESKTOP_PROBE, {
35
+ label:'Windows interactive desktop readiness',
36
+ timeout:6000,
37
+ });
38
+ return classifyWindowsDesktopSession(JSON.parse(String(result?.stdout || '').trim()));
39
+ } catch {
40
+ // A failed OS query is not proof that no user is signed in.
41
+ return { status:'unknown', available:null, reason:'session_probe_failed' };
42
+ }
43
+ }
44
+
45
+ // Tool discovery can request the same session status several times per second.
46
+ // Cache only advertised capabilities; native actions always use a fresh probe.
47
+ export function createWindowsDesktopSessionCache({
48
+ probe = windowsDesktopSessionStatus,
49
+ ttlMs = 2500,
50
+ nowFn = Date.now,
51
+ } = {}) {
52
+ let cached = null;
53
+ let expiresAt = 0;
54
+ let inFlight = null;
55
+ return async () => {
56
+ if (cached && nowFn() < expiresAt) return cached;
57
+ if (inFlight) return inFlight;
58
+ inFlight = Promise.resolve().then(probe).catch(() => ({
59
+ status:'unknown', available:null, reason:'session_probe_failed',
60
+ })).then(result => {
61
+ cached = result && typeof result.available === 'boolean'
62
+ ? result
63
+ : { status:'unknown', available:null, reason:'session_probe_failed' };
64
+ expiresAt = nowFn() + Math.max(0, ttlMs);
65
+ return cached;
66
+ }).finally(() => { inFlight = null; });
67
+ return inFlight;
68
+ };
69
+ }
70
+
71
+ export const cachedWindowsDesktopSessionStatus = createWindowsDesktopSessionCache();
72
+
73
+ export async function assertWindowsInteractiveDesktop({
74
+ platform = process.platform,
75
+ probe = windowsDesktopSessionStatus,
76
+ } = {}) {
77
+ if (platform !== 'win32') return;
78
+ const status = await probe();
79
+ if (status?.available === true) return;
80
+ const reason = status?.reason || 'session_probe_failed';
81
+ throw new Error(`Windows desktop unavailable (${reason}): sign in to an interactive Windows desktop before using native screen or input tools.`);
82
+ }
83
+
84
+ export function guardWindowsDesktopTool(handler, options = {}) {
85
+ return async (...args) => {
86
+ await assertWindowsInteractiveDesktop(options);
87
+ return handler(...args);
88
+ };
89
+ }
@@ -9,6 +9,7 @@ import { isWaylandSession } from '../screenshot-portal.mjs';
9
9
  import { openDirectoryPath } from '../tools/files.mjs';
10
10
  import { resolveSafePath } from '../util.mjs';
11
11
  import { clamp, commandExists, jsonResult, runFile, unavailable } from './common.mjs';
12
+ import { assertWindowsInteractiveDesktop } from './desktop-windows-session.mjs';
12
13
 
13
14
  export function resolveRecordScreenFfmpeg({
14
15
  platform = process.platform,
@@ -330,6 +331,8 @@ async function copyRecordingFile(source, destination) {
330
331
  }
331
332
 
332
333
  export async function recordScreen(args) {
334
+ // Reject Windows Session 0 before creating temporary files or starting ffmpeg.
335
+ await assertWindowsInteractiveDesktop();
333
336
  const seconds = clamp(args.duration_seconds, 5, 1, 120);
334
337
  const fps = clamp(args.fps, 15, 1, 60);
335
338
  const wayland = process.platform === 'linux' && isWaylandSession();
package/src/index.mjs CHANGED
@@ -2,8 +2,9 @@
2
2
  import process from 'node:process';
3
3
  import { mkdirSync, writeFileSync } from 'node:fs';
4
4
  import path from 'node:path';
5
- import { advertisedTools as advertisedCoreTools, supportedCoreTools, toolDefinitions as coreToolDefinitions } from './catalog.mjs';
6
- import { allExtendedTools, advertisedExtendedTools, extendedToolDefinitions } from './extended/catalog.mjs';
5
+ import { advertisedTools as advertisedCoreTools, toolDefinitions as coreToolDefinitions } from './catalog.mjs';
6
+ import { allExtendedTools, extendedToolDefinitions } from './extended/catalog.mjs';
7
+ import { supportedRuntimeToolDefinitions } from './tool-availability.mjs';
7
8
  import { describeConfig, configurationError, runtimeConfigDir } from './config.mjs';
8
9
  import { shutdownSessions, startSessionSweeper } from './sessions.mjs';
9
10
  import { flush, setTelemetrySink, shutdownTelemetry, telemetryEnabled } from './telemetry.mjs';
@@ -13,7 +14,8 @@ const args = process.argv.slice(2);
13
14
  const allToolDefinitions = [...coreToolDefinitions, ...extendedToolDefinitions];
14
15
 
15
16
  async function advertisedRuntimeTools() {
16
- return [...supportedCoreTools(), ...await advertisedExtendedTools()];
17
+ const { core, extended } = await supportedRuntimeToolDefinitions();
18
+ return [...core, ...extended];
17
19
  }
18
20
 
19
21
  async function writeStdout(value) {
package/src/mcp-v2.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import process from 'node:process';
2
- import { supportedCoreTools, toolDefinitions, TEXT_OUTPUT_SCHEMA } from './catalog.mjs';
3
- import { advertisedExtendedTools, extendedToolDefinitions } from './extended/catalog.mjs';
2
+ import { toolDefinitions, TEXT_OUTPUT_SCHEMA } from './catalog.mjs';
3
+ import { extendedToolDefinitions } from './extended/catalog.mjs';
4
+ import { supportedRuntimeToolDefinitions } from './tool-availability.mjs';
4
5
  import { invokeTool } from './invoke.mjs';
5
6
 
6
7
  const DEFAULT_CAPABILITY_POLL_MS = 3000;
@@ -26,10 +27,8 @@ export async function startRuntimeMcpServer({ version, instructions, onError = (
26
27
  import('@modelcontextprotocol/server/stdio'),
27
28
  ]);
28
29
 
29
- const initiallySupported = new Set([
30
- ...supportedCoreTools().map(tool => tool.name),
31
- ...(await advertisedExtendedTools()).map(tool => tool.name),
32
- ]);
30
+ const initial = await supportedRuntimeToolDefinitions();
31
+ const initiallySupported = new Set([...initial.core, ...initial.extended].map(tool => tool.name));
33
32
  let activeServer = null;
34
33
  let capabilityTimer = null;
35
34
  let capabilityPollRunning = false;
@@ -69,9 +68,10 @@ export async function startRuntimeMcpServer({ version, instructions, onError = (
69
68
  if (closed || capabilityPollRunning) return;
70
69
  capabilityPollRunning = true;
71
70
  try {
72
- const next = new Set((await advertisedExtendedTools()).map(tool => tool.name));
71
+ const updated = await supportedRuntimeToolDefinitions();
72
+ const next = new Set([...updated.core, ...updated.extended].map(tool => tool.name));
73
73
  let changed = false;
74
- for (const definition of extendedToolDefinitions) {
74
+ for (const definition of [...toolDefinitions, ...extendedToolDefinitions]) {
75
75
  const before = lastSupported.has(definition.name);
76
76
  const after = next.has(definition.name);
77
77
  if (before === after) continue;
@@ -0,0 +1,23 @@
1
+ import process from 'node:process';
2
+ import { supportedCoreTools } from './catalog.mjs';
3
+ import { advertisedExtendedTools, capabilitySnapshot } from './extended/catalog.mjs';
4
+
5
+ // The core screenshot tool is distinct from extended screenshot_region, but
6
+ // both require a real Windows user desktop. Keep read_image and all other
7
+ // filesystem tools available to Session 0 service agents.
8
+ export function filterCoreToolsForLiveDesktop(tools, {
9
+ platform = process.platform,
10
+ capabilities = {},
11
+ } = {}) {
12
+ if (platform !== 'win32' || capabilities.desktop_session === true) return tools;
13
+ return tools.filter(tool => tool.name !== 'take_screenshot');
14
+ }
15
+
16
+ export async function supportedRuntimeToolDefinitions() {
17
+ // One consistent OS capability snapshot for ordinary and compact MCP lists.
18
+ const capabilities = await capabilitySnapshot();
19
+ return {
20
+ core:filterCoreToolsForLiveDesktop(supportedCoreTools(), { capabilities }),
21
+ extended:await advertisedExtendedTools(capabilities),
22
+ };
23
+ }
@@ -11,6 +11,7 @@ import { documentKind, readDocxText, readPdfText } from '../documents.mjs';
11
11
  import { diffStats, unifiedDiff } from '../diff.mjs';
12
12
  import { describeFilesystemFailure } from '../permissions.mjs';
13
13
  import { capturePortalScreenshot, isWaylandSession } from '../screenshot-portal.mjs';
14
+ import { assertWindowsInteractiveDesktop } from '../extended/desktop-windows-session.mjs';
14
15
  import { applyHunks, parseUnifiedDiff } from '../patch.mjs';
15
16
  import { countEvent, recordEvent } from '../telemetry.mjs';
16
17
  import { clampInteger, decodeText, displayPath, fail, fitsOutput, globToRegExp, image, isInsideRoot, looksBinary, multi, pageLines, resolveSafePath, splitLines, text, throwIfCancelled } from '../util.mjs';
@@ -98,6 +99,8 @@ export async function runWindowsScreenshot(file, runProcess = execFileAsync) {
98
99
  }
99
100
 
100
101
  export async function takeScreenshotTool(args) {
102
+ // Reject Windows Session 0 before allocating files or running pixel capture.
103
+ await assertWindowsInteractiveDesktop();
101
104
  const requestedDirectory = args.directory ? await resolveSafePath(args.directory, 'directory') : '';
102
105
  const temporaryDirectory = requestedDirectory ? '' : await mkdtemp(path.join(os.tmpdir(), 'remcp-screenshot-'));
103
106
  const directory = requestedDirectory || temporaryDirectory;