@remcp/runtime 0.2.47 → 0.2.48

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.
@@ -0,0 +1,564 @@
1
+ import process from 'node:process';
2
+ const JSON_OUTPUT_SCHEMA = {
3
+ type: 'object',
4
+ additionalProperties: true,
5
+ description: 'Structured tool result. Array/scalar payloads are exposed under data for compatibility with MCP clients that require object-root output schemas.',
6
+ };
7
+ import { browserCapabilityAvailable, browserHandlers } from './browser.mjs';
8
+ import { isWaylandSession } from '../screenshot-portal.mjs';
9
+ import { waylandPortalCandidate } from '../wayland-remote-desktop.mjs';
10
+ import { commandExists, runFile } from './common.mjs';
11
+ import { desktopHandlers } from './desktop.mjs';
12
+ import { diagnosticHandlers } from './diagnostics.mjs';
13
+ import { documentHandlers } from './documents.mjs';
14
+
15
+ const readOnly = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
16
+ const readOnlyLive = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
17
+ const readOnlyOpen = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true };
18
+ const additive = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
19
+ const mutatingNonDestructive = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
20
+ const openMutatingNonDestructive = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true };
21
+ const mutating = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
22
+ const openMutating = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true };
23
+
24
+ const s = description => ({ type: 'string', ...(description ? { description } : {}) });
25
+ const n = description => ({ type: 'number', ...(description ? { description } : {}) });
26
+ const b = description => ({ type: 'boolean', ...(description ? { description } : {}) });
27
+ const e = (values, description) => ({ type: 'string', enum: values, ...(description ? { description } : {}) });
28
+ const o = (properties = {}, required = []) => ({ type: 'object', properties, ...(required.length ? { required } : {}), additionalProperties: false });
29
+
30
+ const AI_TOOL_DESCRIPTIONS = Object.freeze({
31
+ computer_snapshot: 'Use this for the first look at an unfamiliar desktop state when you need one compact bundle of windows, semantic UI, displays, clipboard metadata and optional pixels/browser/OCR. Prefer it over repeated screenshots; narrow later with ui_find, browser_find or screenshot_region.',
32
+ computer_action: 'Use this only when one semantic click/edit/focus/selection action must work across an uncertain target backend and may need Accessibility → browser DOM/CDP → OCR → coordinate fallback, or when multi_select/multi_edit avoids many identical native UI calls. Do not use it when the backend or operation is already known: use ui_action for native controls, browser_action for page DOM, type_text for prose, keyboard for shortcuts, pointer for raw pointer gestures, drag_drop or scroll for those gestures, window_action for windows, and the dedicated clipboard/launch/open/reveal/notification tools for those jobs.',
33
+ list_windows: 'Use this to discover top-level desktop windows or obtain a stable window id/PID/bounds before window_action, screenshot_region or targeted input. Prefer it over screenshot-based window guessing.',
34
+ window_action: 'Use this to focus, minimize, maximize, restore, move, resize or close one known top-level window. Select it by id/PID/app/title from list_windows; do not use pointer coordinates for window management when this semantic tool can express the action.',
35
+ launch_app: 'Use this to start a desktop application by executable/path or friendly application name without a shell. Prefer it over start_process for GUI application launch; use start_process for terminal commands, build tools and shell pipelines.',
36
+ ui_snapshot: 'Use this to inspect native desktop Accessibility/UI Automation semantics for the active app before clicking or typing. Prefer scope=active; use scope=desktop only for cross-application discovery. For a Chromium web page with CDP available, prefer browser_snapshot for page DOM semantics.',
37
+ ui_find: 'Use this after ui_snapshot when you need a small set of native UI elements by role/name/label/AutomationId. Prefer returned id or compact label for subsequent ui_action; refresh only when the UI has changed.',
38
+ ui_action: 'Use this to invoke, focus, edit, select, toggle, expand/collapse, range-edit or scroll a native accessible UI element. Prefer this over pointer clicks because semantic actions survive layout changes; fall back to pointer only when accessibility cannot perform the requested action.',
39
+ type_text: 'Use this for reliable Unicode text entry into a native semantic target or focused control. Prefer method=auto so Accessibility or clipboard restoration preserves the requested characters regardless of keyboard layout. Use method=keys only for deliberate physical key-by-key input; printable output then follows the current keyboard layout on the computer. Use keyboard for shortcuts, navigation keys and control keys.',
40
+ keyboard: 'Use this for shortcuts and individual key presses such as Ctrl/Cmd+C, Tab, Enter or Escape. Do not use it to enter normal prose when type_text is available. On Wayland auto uses an already-authorized portal; backend=portal explicitly requests consent.',
41
+ pointer: 'Use this only when an action truly requires screen coordinates or a semantic UI/browser action is unavailable. Prefer ui_action for native controls and browser_action for web controls; pointer is the coordinate fallback for move/click/button-down/button-up.',
42
+ drag_drop: 'Use this for a real drag gesture between two UI element ids or coordinates. Prefer element ids from ui_find/ui_snapshot because their current bounds are resolved at execution time; use coordinates only when no semantic endpoints exist.',
43
+ scroll: 'Use this for native wheel scrolling of a desktop region or semantic UI target. Prefer browser_action scroll_into_view for a known web element and ui_action scroll_into_view for a known accessible desktop element.',
44
+ wait_for_ui: 'Use this after a desktop action to wait for a native UI element/window/focus/text condition instead of sleeping or polling screenshots. Prefer semantic presence/absence/change conditions and set a bounded timeout.',
45
+ clipboard: 'Use this for explicit text clipboard read/write/clear operations. Do not use clipboard as a typing workaround directly; type_text already uses safe clipboard paste with restoration when needed.',
46
+ display_inventory: 'Use this before monitor-specific screenshots, cross-monitor geometry or coordinate work to obtain monitor bounds, scale and primary-display information.',
47
+ screenshot_region: 'Use this when pixels are required for visual verification but only one window, monitor or rectangle matters. Prefer this over a full-desktop screenshot; use ui_snapshot/browser_snapshot first when semantic structure is sufficient.',
48
+ open_path: 'Use this to open a permitted file or directory in its normal desktop application. Prefer it over shell-specific open/start/xdg-open commands because ReMCP applies filesystem confinement.',
49
+ reveal_path: 'Use this to reveal a permitted local file or directory in Finder/Explorer/file manager without opening the document itself.',
50
+ notification: 'Use this only when the user wants a visible native desktop notification on the paired computer. Do not use it as a substitute for replying in chat.',
51
+ browser_tabs: 'Use this first for CDP browser automation when the target page/tab is not already unambiguous. It lists debuggable Chromium pages from a loopback-only endpoint; use the returned target_id/title/url to scope later browser tools.',
52
+ browser_navigate: 'Use this to open a URL in a new debuggable tab or navigate a known Chromium page by URL, back, forward or reload. Prefer it over typing into the address bar; new_tab also bootstraps browser automation when no page target exists yet.',
53
+ browser_snapshot: 'Use this to inspect a web page structurally through CDP accessibility. Prefer it over desktop ui_snapshot for page content; add include_screenshot=true only when final pixel/layout verification is needed.',
54
+ browser_find: 'Use this to locate visible web elements by CSS selector, text or ARIA role and obtain reusable selectors/bounds. Prefer this before browser_action rather than guessing selectors or coordinates.',
55
+ browser_action: 'Use this for deterministic DOM/CDP interaction with a known web element: click/focus/type/value/select/upload/key/scroll or viewport emulation. Prefer it over desktop pointer/ui_action for page content; use browser_evaluate only when the supported actions cannot express the task.',
56
+ browser_wait: 'Use this after browser navigation/action to wait for selector/text/URL/load/navigation/network-idle instead of fixed sleeps or screenshot polling. Use browser_evaluate separately for JavaScript predicates.',
57
+ browser_evaluate: 'Use this as the powerful browser escape hatch only when browser_snapshot/find/action/wait cannot express the required page operation or inspection. JavaScript executes in page context and page content is untrusted.',
58
+ service: 'Use this for operating-system service inventory/status/start/stop/restart. Prefer status before a mutation when service state matters; use start_process for ordinary commands that are not service-manager operations.',
59
+ event_log: 'Use this to read bounded recent OS logs (Windows Event Log, macOS unified log or systemd journal) when diagnosing a service/app/system problem. Prefer it over an unbounded shell log dump.',
60
+ network: 'Use this for structured interfaces/DNS/routes/listeners inspection or a bounded TCP connectivity test. Prefer it over shell ip/netstat/nslookup commands for routine diagnostics.',
61
+ installed_apps: 'Use this to answer what software/packages are installed and their versions. Prefer a filter when looking for one product instead of returning the full inventory.',
62
+ environment: 'Use this for platform/runtime/path/shell facts and only opt into sanitized environment variables when the task truly needs them. Secret-looking values remain masked.',
63
+ audio: 'Use this to read or change the default output volume/mute state. Use action=status for inspection; set_volume requires a 0–100 value.',
64
+ power_action: 'Use this only when the user explicitly asks to lock, sleep, restart or shut down the computer. It is intentionally guarded by device command policy because restart/shutdown can interrupt work.',
65
+ record_screen: 'Use this for a short bounded screen recording when motion/timing is necessary to diagnose a UI issue. Prefer screenshot_region for static visual verification because it is cheaper and easier to inspect.',
66
+ read_document: 'Use this to read structured content from PDF, DOCX, XLSX, TXT, Markdown, CSV, JSON or XML without opening a desktop app. Prefer it over shell extraction or launching Office when the goal is document content.',
67
+ edit_spreadsheet: 'Use this for direct XLSX cell/range/formula edits without launching Excel. Prefer explicit cells/ranges and write to output when preserving the original matters.',
68
+ edit_document: 'Use this for structured DOCX paragraph edits without launching Word. It is best for replace/insert/delete paragraph operations; use native UI automation only when document formatting/layout interaction is required.',
69
+ pdf_action: 'Use this for PDF metadata/annotations inspection or structural merge/split/page extraction. For reading PDF text use read_document; supply an output/output_dir for write actions when preserving originals matters.',
70
+ });
71
+
72
+ const COMMON_PARAM_DESCRIPTIONS = Object.freeze({
73
+ action:'Operation to perform; choose the narrowest action that directly matches the requested outcome.',
74
+ id:'Stable target id returned by the corresponding discovery tool.',
75
+ label:'Compact numeric accessibility label from the most recent semantic UI snapshot/find.',
76
+ pid:'Owning process id used to scope the target.',
77
+ app:'Application/process name or substring used to select the target.',
78
+ title:'Window/page title substring used to select the target.',
79
+ window_title:'Top-level accessibility window/frame title substring used to scope native UI matching.',
80
+ name:'Accessible element name/title substring.',
81
+ role:'Accessible or ARIA role used to narrow semantic matching.',
82
+ automation_id:'Windows UI Automation AutomationId when available.',
83
+ x:'Virtual-desktop X coordinate in pixels.',
84
+ y:'Virtual-desktop Y coordinate in pixels.',
85
+ width:'Width in pixels.',
86
+ height:'Height in pixels.',
87
+ value:'Value required by the selected action.',
88
+ text:'Text required by the selected action.',
89
+ selector:'CSS selector identifying a web element.',
90
+ endpoint:'Loopback Chrome DevTools Protocol endpoint; normally leave unset to use the default.',
91
+ target_id:'Browser page target id returned by browser_tabs.',
92
+ url_contains:'Substring used to select a browser page by URL.',
93
+ timeout_ms:'Bounded timeout in milliseconds.',
94
+ poll_ms:'Polling interval in milliseconds.',
95
+ path:'Permitted local filesystem path.',
96
+ output:'Optional permitted output path; when omitted the tool may update the input according to its action.',
97
+ output_dir:'Permitted directory for generated output files.',
98
+ paths:'List of permitted local filesystem paths.',
99
+ args:'Argument array passed directly to the launched application; no shell parsing.',
100
+ backend:'Input backend preference; auto is recommended unless a specific backend is required.',
101
+ key:'Single key name such as Enter, Escape, Tab or A.',
102
+ shortcut:'Keyboard shortcut such as CTRL+L, CMD+K or ALT+F4.',
103
+ delta_x:'Horizontal wheel/scroll delta; positive values move right.',
104
+ delta_y:'Vertical wheel/scroll delta; positive values move down.',
105
+ from_x:'Source X coordinate for a drag gesture.',
106
+ from_y:'Source Y coordinate for a drag gesture.',
107
+ to_x:'Destination X coordinate for a drag gesture.',
108
+ to_y:'Destination Y coordinate for a drag gesture.',
109
+ from_id:'Source semantic UI element id; preferred over source coordinates when available.',
110
+ to_id:'Destination semantic UI element id; preferred over destination coordinates when available.',
111
+ limit:'Maximum number of results to return.',
112
+ filter:'Optional bounded filter expression or text used to reduce returned results.',
113
+ query:'Optional platform-specific query used to reduce returned log results.',
114
+ scope:'Scope of the operation; choose user scope unless the task explicitly requires a system service.',
115
+ host:'Host name or IP address for a bounded connectivity test.',
116
+ port:'TCP port number for a connectivity test.',
117
+ duration_seconds:'Bounded recording/action duration in seconds.',
118
+ fps:'Frames per second for screen recording.',
119
+ sheet:'Worksheet name.',
120
+ max_cells:'Maximum number of spreadsheet cells to return.',
121
+ pattern:'Output filename pattern where supported.',
122
+ pages:'PDF page numbers/ranges such as 1-3,5.',
123
+ message:'User-visible notification text.',
124
+ max_nodes:'Maximum accessibility nodes to inspect or return; keep this bounded and raise it only when the target is missing.',
125
+ max_depth:'Maximum accessibility tree depth to traverse.',
126
+ method:'Text-entry strategy. auto is recommended; accessibility is semantic, clipboard preserves Unicode, and keys emits physical key events whose printable characters follow the computer current keyboard layout.',
127
+ caret_position:'Where text insertion should occur when the semantic editor supports caret placement.',
128
+ keys:'Ordered key names that together form the requested shortcut or key sequence.',
129
+ button:'Pointer button to use for click/down/up or drag gestures.',
130
+ hold_ms:'How long to hold the pointer button before movement during a drag.',
131
+ duration_ms:'Duration of the drag/movement gesture in milliseconds.',
132
+ replacement:'Replacement text for matching document content.',
133
+ direction:'Human-readable scroll direction used with wheel_times instead of explicit deltas.',
134
+ condition:'Semantic condition to wait for; choose the condition that directly proves the requested state transition.',
135
+ timeout_seconds:'Bounded native notification display timeout in seconds when the platform supports it.',
136
+ text_value:'Text value used by browser type/set-value actions; use text or value when possible unless the action specifically expects this compatibility alias.',
137
+ option:'Option value to select in a browser select element.',
138
+ expression:'JavaScript expression or predicate used only by the selected browser operation/wait condition.',
139
+ edits:'Bounded list of spreadsheet edits; each item names a cell/range and the value(s) or formula to write.',
140
+ operations:'Bounded list of structured document operations executed in order.',
141
+ });
142
+
143
+ function enrichSchemaDescriptions(schema) {
144
+ if (!schema || typeof schema !== 'object') return schema;
145
+ const copy = structuredClone(schema);
146
+ const visit = node => {
147
+ if (!node || typeof node !== 'object') return;
148
+ if (node.properties && typeof node.properties === 'object') {
149
+ for (const [key, property] of Object.entries(node.properties)) {
150
+ if (property && typeof property === 'object' && !property.description) {
151
+ property.description = COMMON_PARAM_DESCRIPTIONS[key] || ('Parameter ' + key.replaceAll('_',' ') + ' for this operation.');
152
+ }
153
+ visit(property);
154
+ }
155
+ }
156
+ if (node.items) visit(node.items);
157
+ for (const key of ['oneOf','anyOf','allOf']) if (Array.isArray(node[key])) node[key].forEach(visit);
158
+ };
159
+ visit(copy);
160
+ return copy;
161
+ }
162
+
163
+ const outputString = description => ({ type:'string', ...(description ? { description } : {}) });
164
+ const outputNumber = description => ({ type:'number', ...(description ? { description } : {}) });
165
+ const outputBoolean = description => ({ type:'boolean', ...(description ? { description } : {}) });
166
+ const outputObject = description => ({ type:'object', additionalProperties:true, ...(description ? { description } : {}) });
167
+ const outputRecord = (properties = {}, description) => ({ type:'object', properties, additionalProperties:true, ...(description ? { description } : {}) });
168
+ const outputArray = (items = {}, description) => ({ type:'array', items, ...(description ? { description } : {}) });
169
+
170
+ const OUTPUT_FIELDS = Object.freeze({
171
+ computer_snapshot: {
172
+ device:outputObject('Device/platform metadata for the captured computer state.'),
173
+ windows:outputArray(outputObject(), 'Top-level windows with stable ids/PIDs/bounds when available.'),
174
+ displays:outputArray(outputObject(), 'Display inventory used for virtual-desktop geometry.'),
175
+ active_window:outputObject('Active top-level window when known.'),
176
+ ui:outputObject('Native accessibility snapshot and semantic tree.'),
177
+ cursor:outputObject('Pointer position when available.'),
178
+ clipboard:outputObject('Clipboard metadata only unless clipboard content was explicitly requested.'),
179
+ browser:outputObject('Optional browser DOM/CDP snapshot.'),
180
+ ocr:outputObject('Optional OCR words/metadata.'),
181
+ fallback_chain:outputObject('Which semantic/DOM/OCR/coordinate layers were requested and available.'),
182
+ errors:outputArray(outputObject(), 'Non-fatal sub-capture errors.'),
183
+ },
184
+ computer_action: { action:outputString(), ok:outputBoolean(), completed:outputNumber(), results:outputArray(outputObject()), stopped_at:outputNumber(), count:outputNumber(), waited_ms:outputNumber(), capped:outputBoolean() },
185
+ list_windows: { data:outputArray(outputRecord({ id:outputString('Stable window id.'), pid:outputNumber('Owning process id.'), app:outputString(), title:outputString(), x:outputNumber(), y:outputNumber(), width:outputNumber(), height:outputNumber(), monitor:{ type:['string','number','null'], description:'Monitor name or platform index when available.' }, active:outputBoolean(), minimized:outputBoolean(), maximized:outputBoolean() }, 'Top-level window record.'), 'Top-level windows.') },
186
+ window_action: { action:outputString(), id:outputString(), pid:outputNumber(), title:outputString(), text:outputString() },
187
+ launch_app: { text:outputString('Human-readable launch result and optional ready-window confirmation.') },
188
+ ui_snapshot: { platform:outputString(), count:outputNumber(), nodes:outputArray(outputRecord({ id:outputString('Stable accessibility element id.'), label:outputNumber('Compact label valid for the current cached snapshot.'), role:outputString(), name:outputString(), value:{ type:['string','number','boolean','null'], description:'Accessible value in its native scalar type when available.' }, x:outputNumber(), y:outputNumber(), width:outputNumber(), height:outputNumber(), focused:outputBoolean(), enabled:outputBoolean() }, 'Accessible semantic node.')), semantic_tree:outputString(), label_count:outputNumber(), browser_dom:outputObject() },
189
+ ui_find: { count:outputNumber(), nodes:outputArray(outputRecord({ id:outputString('Stable accessibility element id.'), label:outputNumber('Compact snapshot label.'), role:outputString(), name:outputString(), value:{ type:['string','number','boolean','null'], description:'Accessible value in its native scalar type when available.' }, x:outputNumber(), y:outputNumber(), width:outputNumber(), height:outputNumber() }, 'Matched accessible node.')) },
190
+ ui_action: { action:outputString(), id:outputString(), label:outputNumber(), backend:outputString(), result:outputObject(), text:outputString() },
191
+ type_text: { length:outputNumber(), method:outputString(), target:outputObject(), clear:outputBoolean(), press_enter:outputBoolean(), clipboard_restored:outputBoolean(), backend:outputString() },
192
+ keyboard: { action:outputString(), backend:outputString(), text:outputString() },
193
+ pointer: { action:outputString(), x:outputNumber(), y:outputNumber(), button:outputString(), backend:outputString(), id:outputString() },
194
+ drag_drop: { from:outputArray(outputNumber()), to:outputArray(outputNumber()), from_element:outputString(), to_element:outputString(), backend:outputString() },
195
+ scroll: { delta_x:outputNumber(), delta_y:outputNumber(), backend:outputString(), text:outputString() },
196
+ wait_for_ui: { state:outputString(), wanted:outputString(), matched:{ type:['number','boolean'], description:'Match count for state waits or boolean result for condition waits.' }, attempts:outputNumber(), elapsed_ms:outputNumber(), nodes:outputArray(outputObject()), before:outputObject() },
197
+ clipboard: { action:outputString(), length:outputNumber(), backend:outputString(), text:outputString(), data:outputString() },
198
+ display_inventory: { data:outputArray(outputObject('Display with id/name/virtual bounds/scale/primary state.')) },
199
+ screenshot_region: { text:outputString('Capture summary; image bytes are returned as MCP image content.'), image:outputObject('Optional structured image metadata when the client mirrors image content.') },
200
+ open_path: { text:outputString() },
201
+ reveal_path: { text:outputString() },
202
+ notification: { text:outputString() },
203
+ browser_tabs: { data:outputArray(outputRecord({ id:outputString('Stable CDP page target id.'), title:outputString(), url:outputString(), type:outputString(), webSocketDebuggerUrl:outputString() }, 'Debuggable browser page target.')) },
204
+ browser_navigate: { target_id:outputString('Stable CDP page target id.'), action:outputString(), url:outputString(), title:outputString(), frame_id:{ type:['string','null'], description:'CDP frame id when navigation produced one.' }, error_text:{ type:['string','null'], description:'CDP navigation error text, or null when navigation succeeded.' } },
205
+ browser_snapshot: { target_id:outputString('Stable CDP page target id.'), url:outputString(), title:outputString(), count:outputNumber(), nodes:outputArray(outputObject('Accessibility node for the page.')), semantic_tree:outputString(), screenshot:outputObject() },
206
+ browser_find: { target_id:outputString(), count:outputNumber(), matches:outputArray(outputRecord({ selector:outputString('Reusable CSS selector when one can be derived.'), role:outputString(), text:outputString(), x:outputNumber(), y:outputNumber(), width:outputNumber(), height:outputNumber() }, 'Matched DOM element.')) },
207
+ browser_action: { target_id:outputString(), action:outputString(), selector:outputString(), key:outputString(), files:outputNumber(), width:outputNumber(), height:outputNumber(), device_scale_factor:outputNumber(), mobile:outputBoolean(), result:outputObject() },
208
+ browser_wait: { target_id:outputString(), condition:outputString(), matched:outputBoolean(), attempts:outputNumber(), elapsed_ms:outputNumber(), value:outputObject(), url:outputString(), text:outputString() },
209
+ browser_evaluate: { target_id:outputString(), value:{} },
210
+ service: { data:outputArray(outputObject('Service record.')), action:outputString(), name:outputString(), text:outputString() },
211
+ event_log: { text:outputString('Bounded log output.'), data:outputArray(outputObject()) },
212
+ network: { hostname:outputString(), interfaces:outputObject(), dns:outputArray(outputString()), servers:outputArray(outputString()), host:outputString(), port:outputNumber(), ok:outputBoolean(), latency_ms:outputNumber(), data:outputArray(outputObject()) },
213
+ installed_apps: { data:outputArray(outputObject('Installed app/package with name/version/path where available.')) },
214
+ environment: { platform:outputString(), arch:outputString(), hostname:outputString(), release:outputString(), shell:outputString(), path:outputString(), node:outputString(), env:outputObject() },
215
+ audio: { volume:outputNumber(), muted:outputBoolean(), action:outputString(), text:outputString() },
216
+ power_action: { text:outputString(), action:outputString() },
217
+ record_screen: { path:outputString(), bytes:outputNumber(), duration_seconds:outputNumber(), format:outputString() },
218
+ read_document: { text:outputString(), path:outputString(), data:{}, sheets:outputArray(outputObject()), sheet:outputString(), rows:outputArray({}), cells:outputArray({}) },
219
+ edit_spreadsheet: { path:outputString(), sheet:outputString(), edited_cells:outputNumber(), bytes:outputNumber() },
220
+ edit_document: { path:outputString(), operations:outputNumber(), changes:outputNumber(), bytes:outputNumber() },
221
+ pdf_action: { action:outputString(), path:outputString(), source:outputString(), output:outputString(), output_dir:outputString(), files:outputArray(outputString()), pages:outputString(), bytes:outputNumber(), annotations:outputArray(outputObject()), inputs:outputNumber() },
222
+ });
223
+
224
+ function outputSchemaFor(name, title) {
225
+ const properties = {
226
+ ...(OUTPUT_FIELDS[name] || {}),
227
+ truncated:{ type:'boolean', description:'True when the structured mirror was bounded because the result was large.' },
228
+ bytes:{ type:'number', description:'Serialized structured-result size when truncation metadata is returned.' },
229
+ preview:{ type:'string', description:'Bounded JSON preview when the full structured mirror is too large.' },
230
+ data:(OUTPUT_FIELDS[name] || {}).data || { description:'Array/scalar payload wrapper used when the natural result is not an object.' },
231
+ text:(OUTPUT_FIELDS[name] || {}).text || { type:'string', description:'Human-readable structured mirror for text-only results.' },
232
+ };
233
+ return {
234
+ type:'object',
235
+ properties,
236
+ additionalProperties:true,
237
+ description:title + ' structured result for ' + name + '. Stable fields are declared explicitly; platform-specific fields may be added. Large results may return truncated/bytes/preview instead of the full payload.',
238
+ };
239
+ }
240
+
241
+ function requiredAny(...groups) {
242
+ return groups.map(required => ({ required }));
243
+ }
244
+
245
+ function applySchemaRules(name, schema) {
246
+ const next = structuredClone(schema);
247
+ if (name === 'launch_app') {
248
+ next.anyOf = requiredAny(['app'], ['path']);
249
+ } else if (name === 'keyboard') {
250
+ next.anyOf = requiredAny(['shortcut'], ['key'], ['keys']);
251
+ if (next.properties?.keys) next.properties.keys.minItems = 1;
252
+ } else if (name === 'drag_drop') {
253
+ next.allOf = [
254
+ { anyOf: requiredAny(['from_id'], ['from_x','from_y']) },
255
+ { anyOf: requiredAny(['to_id'], ['to_x','to_y']) },
256
+ ];
257
+ } else if (name === 'screenshot_region') {
258
+ next.anyOf = [
259
+ { required:['x','y','width','height'] },
260
+ ...requiredAny(['window_id'], ['pid'], ['app'], ['title'], ['monitor'], ['monitor_index']),
261
+ ];
262
+ } else if (name === 'browser_navigate') {
263
+ next.allOf = [{
264
+ if:{ properties:{ action:{ enum:['url','new_tab'] } }, required:['action'] },
265
+ then:{ required:['url'] },
266
+ }];
267
+ } else if (name === 'browser_action') {
268
+ next.allOf = [
269
+ {
270
+ if:{ properties:{ action:{ enum:['click','focus','type','set_value','select','scroll_into_view'] } }, required:['action'] },
271
+ then:{ anyOf:requiredAny(['selector'], ['text']) },
272
+ },
273
+ {
274
+ if:{ properties:{ action:{ const:'upload' } }, required:['action'] },
275
+ then:{ required:['selector'], anyOf:requiredAny(['path'], ['paths']) },
276
+ },
277
+ {
278
+ if:{ properties:{ action:{ const:'press' } }, required:['action'] },
279
+ then:{ required:['key'] },
280
+ },
281
+ ];
282
+ if (next.properties?.paths) next.properties.paths.minItems = 1;
283
+ } else if (name === 'browser_wait') {
284
+ next.allOf = [
285
+ {
286
+ if:{ not:{ required:['condition'] } },
287
+ then:{ required:['selector'] },
288
+ },
289
+ {
290
+ if:{ properties:{ condition:{ const:'selector' } }, required:['condition'] },
291
+ then:{ required:['selector'] },
292
+ },
293
+ {
294
+ if:{ properties:{ condition:{ const:'text' } }, required:['condition'] },
295
+ then:{ required:['text'] },
296
+ },
297
+ {
298
+ if:{ properties:{ condition:{ const:'url_contains' } }, required:['condition'] },
299
+ then:{ anyOf:requiredAny(['value'], ['text']) },
300
+ },
301
+ ];
302
+ } else if (name === 'service') {
303
+ next.allOf = [{
304
+ if:{ properties:{ action:{ enum:['status','start','stop','restart'] } }, required:['action'] },
305
+ then:{ required:['name'] },
306
+ }];
307
+ } else if (name === 'audio') {
308
+ next.allOf = [{
309
+ if:{ properties:{ action:{ const:'set_volume' } }, required:['action'] },
310
+ then:{ required:['volume'] },
311
+ }];
312
+ } else if (name === 'pdf_action') {
313
+ next.allOf = [
314
+ {
315
+ if:{ properties:{ action:{ enum:['annotations','info','split','extract_pages'] } }, required:['action'] },
316
+ then:{ required:['path'] },
317
+ },
318
+ {
319
+ if:{ properties:{ action:{ const:'merge' } }, required:['action'] },
320
+ then:{ required:['paths','output'] },
321
+ },
322
+ {
323
+ if:{ properties:{ action:{ const:'extract_pages' } }, required:['action'] },
324
+ then:{ required:['pages','output'] },
325
+ },
326
+ ];
327
+ if (next.properties?.paths) {
328
+ next.properties.paths.minItems = 2;
329
+ next.properties.paths.maxItems = 100;
330
+ }
331
+ }
332
+ return next;
333
+ }
334
+
335
+ function define(name, title, description, inputSchema, annotations, handler, requires = []) {
336
+ const enriched = enrichSchemaDescriptions(inputSchema);
337
+ return {
338
+ name,
339
+ title,
340
+ description: AI_TOOL_DESCRIPTIONS[name] || description,
341
+ inputSchema: applySchemaRules(name, enriched),
342
+ annotations,
343
+ outputSchema: outputSchemaFor(name, title),
344
+ handler,
345
+ requires,
346
+ };
347
+ }
348
+
349
+ const windowSelector = {
350
+ id: s('Window id returned by list_windows.'),
351
+ pid: n('Owning process id.'),
352
+ app: s('Application/process name substring.'),
353
+ title: s('Window title substring.'),
354
+ };
355
+ const uiSelector = {
356
+ id: s('Element id returned by ui_snapshot or ui_find. Linux ids are stable per-process accessibility paths and legacy linux:<index> ids remain accepted.'),
357
+ label: n('Compact numeric label from the most recent ui_snapshot/ui_find; useful for repeated actions without repeating names or coordinates.'),
358
+ pid: n('Limit matching to one owning process id when available.'),
359
+ app: s('Limit matching to an application/process name substring when available.'),
360
+ window_title: s('Limit matching to a top-level accessibility window/frame title substring when available.'),
361
+ name: s('Accessible name/title substring.'),
362
+ role: s('Accessible role such as Button, Edit, textbox or AXButton.'),
363
+ automation_id: s('Windows AutomationId when available.'),
364
+ };
365
+ const inputBackendSelector = {
366
+ backend: e(['auto','x11','portal'], 'Input backend preference. auto uses proven local backends and already-authorized portal input; portal explicitly requests XDG RemoteDesktop consent on Wayland; x11 forces XWayland/xdotool only for operations where read-back testing shows it is reliable. GNOME Wayland keyboard/text deliberately reject x11.'),
367
+ };
368
+ const inputWindowSelector = {
369
+ window_id: s('Optional target window id returned by list_windows. Useful for deterministic XWayland keyboard input on Wayland.'),
370
+ pid: n('Optional target process id.'),
371
+ app: s('Optional target application/process name substring.'),
372
+ title: s('Optional target window title substring.'),
373
+ };
374
+ const browserTarget = {
375
+ endpoint: s('Loopback Chrome DevTools endpoint. Defaults to REMCP_CDP_URL or http://127.0.0.1:9222.'),
376
+ target_id: s('Page target id returned by browser_tabs.'),
377
+ title: s('Choose a page whose title contains this text.'),
378
+ url_contains: s('Choose a page whose URL contains this text.'),
379
+ timeout_ms: n('Timeout in milliseconds.'),
380
+ };
381
+
382
+ export const extendedToolDefinitions = [
383
+ define('computer_snapshot', 'Computer snapshot', 'Capture one compact computer-use state bundle: displays, visible windows, labeled semantic accessibility state, clipboard metadata, optional native browser accessibility/CDP/OCR, and real screenshot pixels. Use include_ui=false for a fast screenshot-first state; screenshot_window_id/monitor/region scope pixels without adding another tool.', o({ max_ui_nodes:n('Maximum native accessibility nodes; default 800.'), max_ui_depth:n('Maximum native accessibility depth; default 12.'), include_ui:b('Include native accessibility tree; default true. False is the fast screenshot-only path.'), ui_scope:e(['active','desktop'],'Accessibility scope for computer_snapshot. Default active keeps the compact snapshot fast; desktop requests all accessible applications/windows.'), semantic_tree:b('Include compact semantic hierarchy text alongside nodes; default true.'), ui_browser_dom:b('Prefer the native browser accessibility document subtree when the OS exposes one; on Windows this also enables the Firefox MSAA/IAccessible fallback.'), include_screenshot:b('Include real rendered desktop pixels; default true.'), screenshot_region:{type:'array',minItems:4,maxItems:4,items:{type:'number'},description:'Virtual-desktop [left,top,right,bottom] region; takes precedence over full desktop.'}, screenshot_window_id:s('Capture the window returned by list_windows instead of the whole desktop.'), screenshot_monitor:s('Monitor name substring or "primary".'), screenshot_monitor_index:n('Zero-based monitor index.'), screenshot_padding:n('Pixels of context around a targeted screenshot; 0..200.'), grid_columns:n('Optional reference-grid column count returned as visual metadata; 0..20.'), grid_rows:n('Optional reference-grid row count returned as visual metadata; 0..20.'), include_browser:b('Include CDP browser accessibility when available. Default auto: only when the active app looks Chromium-based.'), browser_max_nodes:n('Maximum browser accessibility nodes; default 500, maximum 2000.'), include_ocr:b('Run optional local OCR. Default auto: only when native accessibility and browser DOM are unavailable. Requires tesseract on PATH.'), ocr_language:s('Tesseract language code, default eng; multiple languages may use eng+deu.'), ocr_max_words:n('Maximum OCR word boxes returned; default 2000, maximum 10000.'), ocr_psm:n('Tesseract page segmentation mode, 3..13; default 11.'), ocr_timeout_ms:n('OCR timeout, default 30000 ms, maximum 120000 ms.') }), readOnlyLive, desktopHandlers.computer_snapshot),
384
+ define(
385
+ 'computer_action',
386
+ 'Computer action',
387
+ 'Perform one semantic cross-backend action when the target may require Accessibility → browser DOM/CDP → OCR → coordinate fallback.',
388
+ o({
389
+ action:e([
390
+ 'click','invoke','focus','set_value','select','toggle','expand','collapse',
391
+ 'scroll_into_view','set_range_value','add_to_selection','remove_from_selection',
392
+ 'multi_select','multi_edit',
393
+ ]),
394
+ target:e(
395
+ ['auto','ui','ui_element','browser','ocr','coordinates'],
396
+ 'Optional target type. auto tries native accessibility first, then browser DOM/CDP, OCR text when requested, then coordinates for click.',
397
+ ),
398
+ ...uiSelector,
399
+ title:s('Optional browser page title substring when target=browser.'),
400
+ text:s('Text used by multi_edit or as a compatibility value for a semantic edit.'),
401
+ value:s('New value for set_value, set_range_value, or select.'),
402
+ x:n('Virtual-desktop X coordinate used only by the final click fallback.'),
403
+ y:n('Virtual-desktop Y coordinate used only by the final click fallback.'),
404
+ selector:s('CSS selector when the fallback target is a browser element.'),
405
+ browser_text:s('Visible browser text used to locate a DOM fallback target.'),
406
+ ocr_text:s('Visible text to find in rendered pixels for the OCR click fallback.'),
407
+ ocr_region:{
408
+ type:'array',
409
+ minItems:4,
410
+ maxItems:4,
411
+ items:{type:'number'},
412
+ description:'Optional virtual-desktop [left,top,right,bottom] region to OCR. Prefer bounds from list_windows or computer_snapshot.',
413
+ },
414
+ ocr_language:s('Tesseract language code, default eng.'),
415
+ ocr_psm:n('Tesseract page segmentation mode, default 11.'),
416
+ ocr_timeout_ms:n('OCR timeout in milliseconds.'),
417
+ endpoint:s('Loopback Chrome DevTools endpoint for a browser fallback.'),
418
+ target_id:s('Browser page target id returned by browser_tabs.'),
419
+ url_contains:s('Browser URL substring used to select a page.'),
420
+ option:s('Option value for browser select.'),
421
+ targets:{
422
+ type:'array',
423
+ minItems:1,
424
+ maxItems:100,
425
+ items:o({ ...uiSelector }),
426
+ description:'Native semantic targets for action=multi_select.',
427
+ },
428
+ replace_selection:b('For multi_select, select the first item before adding the rest; default true.'),
429
+ edits:{
430
+ type:'array',
431
+ minItems:1,
432
+ maxItems:100,
433
+ items:o({
434
+ ...uiSelector,
435
+ text:s('Replacement text for this semantic edit.'),
436
+ method:e(['auto','accessibility','clipboard','keys'], 'Text-entry strategy; auto is recommended.'),
437
+ clear:b('Clear the current value before entering text; default true.'),
438
+ }, ['text']),
439
+ description:'Native semantic target + text entries for action=multi_edit.',
440
+ },
441
+ ...inputBackendSelector,
442
+ }, ['action']),
443
+ openMutating,
444
+ desktopHandlers.computer_action,
445
+ ),
446
+ define('list_windows', 'List windows', 'List visible top-level desktop windows with stable-enough identifiers, PID, application name, title, bounds, and the monitor containing the largest part of each window when display geometry is available.', o({ include_monitor:b('Enrich each window with monitor and monitor_index; default true.') }), readOnlyLive, desktopHandlers.list_windows, ['windows']),
447
+ define('window_action', 'Window action', 'Focus, minimize, maximize, restore, move, resize, or close a top-level desktop window selected by id, pid, application name, or title. Native GNOME Wayland focus uses AT-SPI; compositor window-manager actions use the consent-backed XDG RemoteDesktop input path.', o({ action:e(['focus','minimize','maximize','restore','move','resize','move_resize','close']), ...windowSelector, x:n(), y:n(), width:n(), height:n(), ...inputBackendSelector }, ['action']), mutating, desktopHandlers.window_action, ['windows']),
448
+ define('launch_app', 'Launch application', 'Launch a desktop application or executable without invoking a shell. On Windows, executable/path launch falls back to Start Menu/UWP app discovery by friendly name. cwd is confined by the ReMCP filesystem policy and is only accepted when the platform can guarantee the child working directory. Optionally wait until a matching top-level window exists.', o({ app:s('Application name, friendly Start Menu name, or executable path.'), path:s('Alias for app.'), args:{ type:'array', items:{type:'string'} }, cwd:s('Optional permitted working directory for direct executable launch.'), wait_for_window:s('Optional application/title substring that must appear after launch.'), wait_timeout_ms:n('Maximum wait for wait_for_window; default 10000, maximum 120000.') }), openMutatingNonDestructive, desktopHandlers.launch_app),
449
+ define('ui_snapshot', 'UI accessibility snapshot', 'Read the active desktop accessibility/UI Automation tree with compact labels, semantic hierarchy, bounds, state and suggested actions. Default scope is the active application/window; scope=desktop requests the full desktop tree. Explicit pid/app/window_title scopes override the active default. browser_dom asks the OS accessibility provider for the web-document subtree when available.', o({ scope:e(['active','desktop'],'Default active. Use desktop only when cross-application discovery is required.'), pid:n('Optional owning process id scope.'), app:s('Optional application/process name substring scope.'), window_title:s('Optional top-level accessibility window/frame title scope.'), browser_dom:b('Prefer the native browser document accessibility subtree instead of browser chrome when supported.'), semantic_tree:b('Include compact semantic hierarchy text; default true.'), max_nodes:n('Maximum nodes; default 500, maximum 5000.'), max_depth:n('Maximum traversal depth; default 8, maximum 32.') }), readOnlyLive, desktopHandlers.ui_snapshot, ['ui']),
450
+ define('ui_find', 'Find UI elements', 'Find native accessible desktop elements by label/id/name/role/AutomationId. Reuses the latest five-second semantic snapshot for fast repeated targeting; refresh=true forces a new OS accessibility capture.', o({ ...uiSelector, refresh:b('Force a fresh accessibility capture instead of the short-lived snapshot cache.'), limit:n('Maximum matches; default 20.'), max_nodes:n(), max_depth:n() }), readOnlyLive, desktopHandlers.ui_find, ['ui']),
451
+ define('ui_action', 'Act on UI element', 'Act on a native accessibility/UI Automation element by id, label or semantic selector. Supports invoke/focus/value/toggle/selection, expand-collapse, range values and scroll-into-view before falling back to coordinates.', o({ action:e(['click','invoke','focus','set_value','select','toggle','expand','collapse','scroll_into_view','set_range_value','add_to_selection','remove_from_selection']), ...uiSelector, value:{description:'New text or numeric range value.'} }, ['action']), openMutating, desktopHandlers.ui_action, ['ui']),
452
+ define('type_text', 'Type text', 'Enter Unicode text into a semantic accessibility target or focused control. Auto prefers accessibility editing, then reliable clipboard paste with previous clipboard restoration; keys remains available for control-character input.', o({ text:s('Text to enter.'), method:e(['auto','accessibility','clipboard','keys']), clear:b('Clear existing text before input when keyboard/paste fallback is used.'), caret_position:e(['start','idle','end']), press_enter:b('Press Enter after typing.'), delay_ms:n('Per-key delay for method=keys.'), ...uiSelector, window_id:s('Optional XWayland target window id for clipboard/keys methods.'), title:s('Optional target window title substring for clipboard/keys methods.'), ...inputBackendSelector }, ['text']), openMutating, desktopHandlers.type_text, ['input']),
453
+ define('keyboard', 'Keyboard shortcut', 'Send a keyboard shortcut or key press to the active desktop application or an explicitly selected window. On Wayland deterministic XWayland input should use window_id/pid/app/title; backend=portal explicitly requests the consent-backed XDG RemoteDesktop path.', o({ shortcut:s(), key:s(), keys:{ type:'array', items:{type:'string'} }, ...inputWindowSelector, ...inputBackendSelector }), openMutating, desktopHandlers.keyboard, ['input']),
454
+ define('pointer', 'Pointer action', 'Move the pointer or perform click, double-click, right-click, mouse-down, or mouse-up at desktop screen coordinates. Wayland supports XWayland fallback and an explicitly consented XDG RemoteDesktop backend.', o({ action:e(['move','click','double_click','right_click','down','up']), x:n(), y:n(), button:e(['left','right','middle']), ...inputBackendSelector }, ['action']), openMutating, desktopHandlers.pointer, ['pointer']),
455
+ define('drag_drop', 'Drag and drop', 'Drag between desktop coordinates or semantic UI element IDs returned by ui_snapshot/ui_find. Element IDs are resolved to the center of their current accessibility bounds before the native pointer adapter runs.', o({ from_x:n(), from_y:n(), to_x:n(), to_y:n(), from_id:s('Source UI element id; use instead of from_x/from_y.'), to_id:s('Destination UI element id; use instead of to_x/to_y.'), button:e(['left','right','middle']), hold_ms:n(), duration_ms:n(), ...inputBackendSelector }), openMutating, desktopHandlers.drag_drop, ['drag']),
456
+ define('scroll', 'Scroll', 'Scroll vertically or horizontally using native wheel input. Target a semantic UI element/label or x/y first so the wheel is delivered to the intended region; direction+wheel_times is a convenience over normalized deltas.', o({ delta_x:n('Horizontal delta; positive means right.'), delta_y:n('Vertical delta; positive means down.'), delta:n('Alias for delta_y.'), direction:e(['up','down','left','right']), wheel_times:n('Number of 120-unit wheel steps when direction is used; 1..50.'), x:n(), y:n(), ...uiSelector, ...inputBackendSelector }), mutatingNonDestructive, desktopHandlers.scroll, ['pointer']),
457
+ define('wait_for_ui', 'Wait for UI', 'Wait on fresh native accessibility state without screenshot polling. Use state=present/absent/changed or condition=text_exists/active_window/element_exists/element_enabled/focused_element.', o({ state:e(['present','absent','changed'], 'changed captures the first matching semantic state and waits until it differs.'), condition:e(['text_exists','active_window','element_exists','element_enabled','focused_element']), text:s('Text used by text_exists or active_window.'), browser_dom:b('Use native browser document accessibility where supported.'), ...uiSelector, timeout_ms:n('Maximum wait, capped at 120000 ms.'), poll_ms:n('Polling interval, 50..5000 ms.') }), readOnlyLive, desktopHandlers.wait_for_ui, ['ui']),
458
+ define('clipboard', 'Clipboard', 'Read, write, or clear the system text clipboard using native operating-system clipboard facilities.', o({ action:e(['read','write','clear']), text:s('Text to write.') }, ['action']), mutating, desktopHandlers.clipboard, ['clipboard']),
459
+ define('display_inventory', 'Display inventory', 'List connected displays/monitors and their geometry and primary-display status using native display tools.', o(), readOnlyLive, desktopHandlers.display_inventory, ['displays']),
460
+ define('screenshot_region', 'Screenshot region', 'Capture a desktop rectangle, a selected top-level window, or a selected monitor and return it as an inline PNG. This avoids full-desktop screenshots when only one UI area matters.', o({ x:n(), y:n(), width:n(), height:n(), window_id:s('Window id returned by list_windows.'), pid:n(), app:s(), title:s(), monitor:s('Monitor name substring, or "primary".'), monitor_index:n('Zero-based monitor index from display_inventory.'), padding:n('Optional pixels added around the resolved rectangle; 0..200.') }), readOnlyLive, desktopHandlers.screenshot_region, ['screen_region']),
461
+ define('open_path', 'Open path', 'Open a permitted local file or directory in its system-associated desktop application. Filesystem confinement is checked before opening.', o({ path:s('File or directory to open.') }, ['path']), additive, desktopHandlers.open_path),
462
+ define('reveal_path', 'Reveal path', 'Reveal a permitted local file or directory in Finder, Explorer, or the Linux file manager after applying ReMCP filesystem confinement.', o({ path:s('File or directory to reveal.') }, ['path']), additive, desktopHandlers.reveal_path),
463
+ define('notification', 'Desktop notification', 'Show a native desktop notification or user-visible popup on the paired computer.', o({ title:s(), message:s('Notification message.'), timeout_seconds:n() }, ['message']), additive, desktopHandlers.notification, ['notifications']),
464
+
465
+ define('browser_tabs', 'Browser tabs', 'List debuggable Chrome, Edge, or Chromium page targets from a loopback-only Chrome DevTools Protocol endpoint.', o({ ...browserTarget }), readOnlyLive, browserHandlers.browser_tabs, ['browser_cdp']),
466
+ 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']),
467
+ 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 recenters the target before capture and returns its exact viewport bounds.', 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']),
468
+ 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']),
469
+ 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']),
470
+ 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']),
471
+ 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']),
472
+
473
+ define('service', 'Service control', 'List, inspect, start, stop, or restart operating-system services using Service Control Manager, launchctl, or systemd. Mutating actions pass through ReMCP command policy.', o({ action:e(['list','status','start','stop','restart']), name:s('Service/unit/launchd label.'), scope:e(['user','system']) }, ['action']), mutating, diagnosticHandlers.service, ['services']),
474
+ define('event_log', 'Event logs', 'Read recent Windows Event Log, macOS unified log, or systemd journal entries with bounded time and result limits.', o({ log:s('Windows log name.'), since:s('Time window such as 30s, 10m, 2h, or 1d.'), filter:s(), query:s(), limit:n() }), readOnlyLive, diagnosticHandlers.event_log, ['logs']),
475
+ define('network', 'Network diagnostics', 'Inspect interfaces, DNS servers, routes and listening sockets, or perform a bounded TCP connectivity test to a host and port.', o({ action:e(['summary','interfaces','dns','routes','listeners','test']), host:s(), port:n(), timeout_ms:n() }), readOnlyOpen, diagnosticHandlers.network),
476
+ define('installed_apps', 'Installed applications', 'Inventory installed applications or packages with versions and publishers where available using native package/application inventories.', o({ filter:s('Name substring.'), limit:n('Maximum results; default 1000.') }), readOnly, diagnosticHandlers.installed_apps, ['apps']),
477
+ define('environment', 'Runtime environment', 'Report platform, architecture, Node version, working/home/temp paths, shell, PATH, and optionally environment variables. Environment variables are opt-in and secret-looking or credential-bearing values are masked.', o({ include_env:b('Include sanitized environment variables; default false.') }), readOnly, diagnosticHandlers.environment),
478
+ define('audio', 'Audio control', 'Read or change default output volume or mute state using native operating-system audio controls when available.', o({ action:e(['status','set_volume','mute','unmute']), volume:n('0..100 for set_volume.') }, ['action']), mutatingNonDestructive, diagnosticHandlers.audio, ['audio']),
479
+ define('power_action', 'Power action', 'Lock, sleep, restart, or shut down the paired computer using native operating-system power facilities. The action passes through ReMCP command policy.', o({ action:e(['lock','sleep','restart','shutdown']), delay_seconds:n('Optional delay, maximum 3600 seconds.') }, ['action']), mutating, diagnosticHandlers.power_action, ['power']),
480
+ define('record_screen', 'Record screen', 'Record a short bounded desktop video to a permitted local file for debugging and return its path and size. Uses native/available recording helpers.', o({ duration_seconds:n('1..120 seconds.'), fps:n('1..60.'), destination:s('Permitted output path.') }), mutating, diagnosticHandlers.record_screen, ['record']),
481
+
482
+ define('read_document', 'Read document', 'Read PDF, DOCX, XLSX, TXT, Markdown, CSV, JSON, or XML. PDF/DOCX extraction is built in and XLSX is parsed from its OOXML worksheet cells.', o({ path:s('Document path.'), sheet:s('Worksheet name for XLSX.'), max_cells:n() }, ['path']), readOnly, documentHandlers.read_document),
483
+ define('edit_spreadsheet', 'Edit spreadsheet', 'Edit up to 500 expanded XLSX cells directly in OOXML without launching Excel. Each edit may target one cell or a rectangular range, with a scalar fill, a values matrix, or a formula.', o({ path:s('Input .xlsx path.'), output:s('Optional output .xlsx; defaults to replacing input.'), sheet:s('Worksheet name.'), edits:{ type:'array', maxItems:500, items:o({ cell:s('Single A1 cell reference.'), range:s('Rectangular range such as A1:C3.'), value:{ description:'Scalar value for one cell or to fill a range.' }, values:{ type:'array', items:{ type:'array', items:{} }, description:'2D matrix matching the range dimensions.' }, formula:s('Optional formula; for a range it is written to each expanded cell.') }) } }, ['path','edits']), mutating, documentHandlers.edit_spreadsheet, ['ooxml']),
484
+ define('edit_document', 'Edit DOCX document', 'Edit DOCX paragraph structure directly in OOXML: replace text, append/prepend paragraphs, insert before/after matching paragraphs, or delete matching paragraphs.', o({ path:s('Input .docx path.'), output:s('Optional output .docx path.'), operations:{ type:'array', maxItems:100, items:o({ action:e(['replace','append_paragraph','prepend_paragraph','insert_paragraph_before','insert_paragraph_after','delete_paragraph']), search:s('Paragraph text substring for replace/insert/delete operations.'), replacement:s(), all:b('Apply to all matching paragraphs; default true.'), text:s('Paragraph text to append/prepend/insert.') }, ['action']) } }, ['path','operations']), mutating, documentHandlers.edit_document, ['ooxml']),
485
+ define('pdf_action', 'PDF action', 'Inspect PDF annotations or metadata, merge PDFs, split a PDF into pages, or extract selected page ranges. Structural writes use qpdf/poppler/pdftk when installed.', o({ action:e(['merge','split','extract_pages','annotations','info']), path:s(), paths:{type:'array',items:{type:'string'}}, output:s(), output_dir:s(), pattern:s(), pages:s('Page list/ranges such as 1-3,5.') }, ['action']), mutating, documentHandlers.pdf_action),
486
+ ];
487
+
488
+ export const extendedToolHandlers = new Map(extendedToolDefinitions.map(definition => [definition.name, definition]));
489
+
490
+ let pyAtSpiCache = { value: false, expiresAt: 0 };
491
+
492
+ async function linuxHasPyAtSpi() {
493
+ const now = Date.now();
494
+ if (pyAtSpiCache.expiresAt > now) return pyAtSpiCache.value;
495
+ if (!commandExists('python3')) {
496
+ pyAtSpiCache = { value: false, expiresAt: now + 60_000 };
497
+ return false;
498
+ }
499
+ const probe = [
500
+ 'try:',
501
+ ' import pyatspi',
502
+ 'except Exception:',
503
+ ' import gi',
504
+ ' gi.require_version("Atspi","2.0")',
505
+ ' from gi.repository import Atspi',
506
+ ].join('\n');
507
+ const result = await runFile('python3', ['-c', probe], { allowFailure: true, timeout: 2000 });
508
+ pyAtSpiCache = { value: result.code === 0, expiresAt: now + 60_000 };
509
+ return pyAtSpiCache.value;
510
+ }
511
+
512
+ export async function capabilitySnapshot() {
513
+ const win = process.platform === 'win32';
514
+ const mac = process.platform === 'darwin';
515
+ const linux = process.platform === 'linux';
516
+ const pyAtSpi = linux ? await linuxHasPyAtSpi() : false;
517
+ const wayland = linux && isWaylandSession();
518
+ const portalInput = wayland && waylandPortalCandidate();
519
+ const x11Input = linux && !wayland && (commandExists('wdotool') || commandExists('xdotool'));
520
+ const browserCdp = await browserCapabilityAvailable(undefined, 500);
521
+ return {
522
+ windows: win || mac || commandExists('wdotool') || commandExists('wmctrl') || (wayland && pyAtSpi),
523
+ ui: win || mac || pyAtSpi,
524
+ input: win || mac || portalInput || x11Input || (wayland && commandExists('wtype')),
525
+ pointer: win || mac || portalInput || x11Input,
526
+ drag: win || mac || portalInput || x11Input,
527
+ clipboard: win || mac || commandExists('wl-paste') || commandExists('xclip') || commandExists('xsel'),
528
+ displays: win || mac || (wayland && linux) || commandExists('wlr-randr') || commandExists('xrandr'),
529
+ screen_region: win || mac || commandExists('grim') || commandExists('import') || (isWaylandSession() && commandExists('ffmpeg')),
530
+ notifications: win || mac || commandExists('notify-send'),
531
+ browser_cdp: browserCdp,
532
+ services: win || mac || commandExists('systemctl'),
533
+ logs: win || mac || commandExists('journalctl'),
534
+ apps: win || mac || commandExists('dpkg-query') || commandExists('rpm'),
535
+ audio: win || mac || commandExists('wpctl') || commandExists('pactl') || commandExists('amixer'),
536
+ power: win || mac || commandExists('systemctl') || commandExists('loginctl'),
537
+ record: win || mac || commandExists('wf-recorder') || commandExists('ffmpeg'),
538
+ ooxml: win || (commandExists('zip') && commandExists('unzip')),
539
+ };
540
+ }
541
+
542
+ function isSupported(definition, capabilities) {
543
+ return (definition.requires || []).every(name => capabilities[name] !== false);
544
+ }
545
+
546
+ function publicShape(definition) {
547
+ const { name, title, description, inputSchema, annotations, outputSchema } = definition;
548
+ return { name, title, description, inputSchema, annotations, outputSchema };
549
+ }
550
+
551
+ export function allExtendedTools() {
552
+ return extendedToolDefinitions.map(publicShape);
553
+ }
554
+
555
+ export async function advertisedExtendedTools() {
556
+ const capabilities = await capabilitySnapshot();
557
+ return extendedToolDefinitions.filter(definition => isSupported(definition, capabilities)).map(publicShape);
558
+ }
559
+
560
+ export async function capabilityFingerprint() {
561
+ const capabilities = await capabilitySnapshot();
562
+ const names = extendedToolDefinitions.filter(definition => isSupported(definition, capabilities)).map(definition => definition.name);
563
+ return JSON.stringify({ capabilities, names });
564
+ }