@remcp/runtime 0.2.176 → 0.2.178
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/README.md +7 -7
- package/package.json +1 -1
- package/src/compact-catalog.mjs +1 -1
- package/src/extended/browser-actions.mjs +2 -0
- package/src/extended/browser-devtools.mjs +116 -0
- package/src/extended/browser-geometry.mjs +95 -0
- package/src/extended/browser-native-audit.mjs +81 -0
- package/src/extended/catalog.mjs +1 -0
package/README.md
CHANGED
|
@@ -31,8 +31,8 @@ node ./src/compact.mjs --print-tools # compact local façade for scanners/local
|
|
|
31
31
|
|
|
32
32
|
## Tools
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
plus
|
|
34
|
+
84 tools are implemented in this repository: the original 44 filesystem/search/terminal/process tools
|
|
35
|
+
plus 40 computer-use, browser, diagnostics, and document tools. The release contract contains all 84;
|
|
36
36
|
the live local MCP `tools/list` omits optional platform-specific tools that the current computer cannot
|
|
37
37
|
support and emits `notifications/tools/list_changed` when that capability set changes. The stdio server is dual-era: existing 2025-era clients continue to work unchanged, while clients that negotiate MCP `2026-07-28` use `server/discover`, cache hints, unrestricted structured output validation, and `subscriptions/listen` for list-change delivery.
|
|
38
38
|
|
|
@@ -52,10 +52,10 @@ support and emits `notifications/tools/list_changed` when that capability set ch
|
|
|
52
52
|
| Diagnostics | `service`, `event_log`, `network`, `installed_apps`, `environment`, `audio`, `power_action`, `record_screen` — `audio` returns stable structured `volume`, `muted`, `action`, and `backend` fields across supported OS backends |
|
|
53
53
|
| Documents | `read_document`, `edit_spreadsheet`, `edit_document`, `pdf_action` |
|
|
54
54
|
|
|
55
|
-
The hosted ReMCP endpoint does not append those
|
|
55
|
+
The hosted ReMCP endpoint does not append those 84 granular operation names to its discovery list.
|
|
56
56
|
Current production runs with `REMCP_CUSTOM_WIDGETS_ENABLED=false` and exposes **10 hosted tool definitions**,
|
|
57
57
|
all model-visible: 8 device-facing façade tools plus 2 account/fleet tools. Those compact definitions route
|
|
58
|
-
to the same **
|
|
58
|
+
to the same **84 granular runtime operations** after the selected computer's live capabilities are checked.
|
|
59
59
|
If custom widgets are explicitly re-enabled, hosted discovery expands to **15 definitions** by adding
|
|
60
60
|
**5 app-only** UI helpers. Cached legacy clients can still call supported granular aliases without inflating
|
|
61
61
|
discovery for new clients.
|
|
@@ -77,7 +77,7 @@ system tools such as qpdf/poppler when present. PDF text reading uses the built-
|
|
|
77
77
|
`sharp`, or `exceljs`.
|
|
78
78
|
|
|
79
79
|
`--print-tools` prints the exact JSON contract (schemas and annotations) the runtime advertises, and
|
|
80
|
-
`src/catalog.mjs` plus `src/extended/catalog.mjs` are the source of truth for the
|
|
80
|
+
`src/catalog.mjs` plus `src/extended/catalog.mjs` are the source of truth for the 84-tool release surface.
|
|
81
81
|
A running device can advertise a smaller capability-aware subset; the runtime declares
|
|
82
82
|
`tools.listChanged`, emits `notifications/tools/list_changed` when dynamic capabilities change, and the
|
|
83
83
|
agent forwards the refreshed tool names to the hosted relay. Browser/CDP tools are the primary dynamic
|
|
@@ -90,7 +90,7 @@ errors. Linux `display_inventory` follows the real monitor backend chain (GNOME
|
|
|
90
90
|
backend (Wayland grim/GNOME/portal path or X11 ImageMagick), not merely because `ffmpeg` is installed.
|
|
91
91
|
|
|
92
92
|
The generated hosted reference covers the current **10 hosted production definitions**, all model-visible
|
|
93
|
-
and routing to the
|
|
93
|
+
and routing to the 84 granular runtime operations described above; the optional widget-enabled surface is 15.
|
|
94
94
|
The separate compact local stdio entrypoint advertises `read_file` plus up to seven domain tools
|
|
95
95
|
(up to 8 tools total) and keeps those groups capability-aware at runtime with `tools/list_changed`; the paired agent keeps using the capability-aware granular runtime.
|
|
96
96
|
See https://remcp.site/docs#tools and public `docs/TOOLS.md`.
|
|
@@ -256,7 +256,7 @@ MCP server.
|
|
|
256
256
|
|
|
257
257
|
| | Desktop Commander 0.2.50 | ReMCP runtime |
|
|
258
258
|
| --- | --- | --- |
|
|
259
|
-
| Tools | 26, including config mutators and document tooling |
|
|
259
|
+
| Tools | 26, including config mutators and document tooling | 84, including filesystem/terminal plus native desktop UI, browser CDP, diagnostics, and lightweight document operations |
|
|
260
260
|
| Runtime dependencies | 34 (Supabase, Puppeteer/md-to-pdf, `sharp`, `exceljs`, Tiptap, ripgrep download) | 2 production-direct (`@modelcontextprotocol/server` v2, `@jellybrick/dbus-next`); legacy/modern clients are dev-only compatibility tests |
|
|
261
261
|
| Install scripts | `postinstall` posts an install payload that ignores the telemetry setting | none |
|
|
262
262
|
| Telemetry | opt-out, 51 event names, remote feature flags, A/B assignment, third-party processor | opt-out, whitelisted event schema, no endpoint, no flags |
|
package/package.json
CHANGED
package/src/compact-catalog.mjs
CHANGED
|
@@ -18,7 +18,7 @@ export const compactRuntimeGroups = Object.freeze({
|
|
|
18
18
|
]),
|
|
19
19
|
view_image: Object.freeze(['read_image','take_screenshot','screenshot_region']),
|
|
20
20
|
control_browser: Object.freeze([
|
|
21
|
-
'browser_tabs','browser_navigate','browser_snapshot','browser_find','browser_action','browser_wait','browser_evaluate',
|
|
21
|
+
'browser_tabs','browser_navigate','browser_snapshot','browser_find','browser_action','browser_wait','browser_devtools','browser_evaluate',
|
|
22
22
|
]),
|
|
23
23
|
manage_system: Object.freeze([
|
|
24
24
|
'get_system_info','list_processes','kill_process','get_runtime_info','get_runtime_stats','set_config_value',
|
|
@@ -14,6 +14,7 @@ import { callPageFunction, evaluate, withTarget, productionSessionKey } from './
|
|
|
14
14
|
import { rememberBrowserViewport } from './browser-viewport.mjs';
|
|
15
15
|
import { browserNavigate, browserSnapshot, browserTabs, findExpression } from './browser-read.mjs';
|
|
16
16
|
import { executeBrowserWorkflow, validateBrowserWorkflow } from './browser-workflow.mjs';
|
|
17
|
+
import { browserDevtools } from './browser-devtools.mjs';
|
|
17
18
|
|
|
18
19
|
export async function browserFind(args) {
|
|
19
20
|
if (!(args.selector || args.text || args.role)) throw new Error('browser_find requires selector, text, or role; use browser_snapshot to enumerate page structure');
|
|
@@ -387,4 +388,5 @@ export const browserHandlers = {
|
|
|
387
388
|
browser_action: browserAction,
|
|
388
389
|
browser_wait: browserWait,
|
|
389
390
|
browser_evaluate: browserEvaluate,
|
|
391
|
+
browser_devtools: browserDevtools,
|
|
390
392
|
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Native Chrome DevTools Protocol diagnostics. No screenshots, request bodies,
|
|
2
|
+
// authentication headers, cookies, or response bodies are collected.
|
|
3
|
+
import { jsonResult, clamp, requireEnum } from './common.mjs';
|
|
4
|
+
import { withTarget } from './browser-session.mjs';
|
|
5
|
+
import { inspectNativeBrowserAudit } from './browser-native-audit.mjs';
|
|
6
|
+
import { inspectDomGeometry } from './browser-geometry.mjs';
|
|
7
|
+
|
|
8
|
+
const METRIC_NAMES = new Set([
|
|
9
|
+
'Timestamp','Documents','Frames','JSEventListeners','Nodes','LayoutCount',
|
|
10
|
+
'RecalcStyleCount','LayoutDuration','RecalcStyleDuration','ScriptDuration',
|
|
11
|
+
'TaskDuration','JSHeapUsedSize','JSHeapTotalSize','DomContentLoaded','NavigationStart',
|
|
12
|
+
]);
|
|
13
|
+
const HIDDEN = '[redacted]';
|
|
14
|
+
export function safeDevtoolsUrl(value) {
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL(String(value || ''));
|
|
17
|
+
if (!['http:', 'https:'].includes(url.protocol)) return '[non-http resource]';
|
|
18
|
+
// No fragments, credentials or query parameters reach the relay.
|
|
19
|
+
return (url.origin + url.pathname).slice(0, 300);
|
|
20
|
+
} catch { return '[unavailable]'; }
|
|
21
|
+
}
|
|
22
|
+
export function safeDevtoolsMessage(value) {
|
|
23
|
+
let text = String(value ?? '').slice(0, 700);
|
|
24
|
+
text = text.replace(/\b(Bearer|Basic)\s+[a-z\d+/_=.-]+/gi, '$1 ' + HIDDEN)
|
|
25
|
+
.replace(/\b(?:eyJ[a-zA-Z0-9_-]{12,}\.){2}[a-zA-Z0-9_-]+\b/g, HIDDEN)
|
|
26
|
+
.replace(/([?&](?:token|key|secret|password|session|code|authorization)=)[^\s&#]+/gi, '$1' + HIDDEN)
|
|
27
|
+
.replace(/\b(?:password|access_token|refresh_token|api_key|client_secret)\s*[:=]\s*[^\s,;&]+/gi, HIDDEN);
|
|
28
|
+
return text.slice(0, 500);
|
|
29
|
+
}
|
|
30
|
+
export function normalizeDevtoolsMetrics(entries) {
|
|
31
|
+
return Object.fromEntries((Array.isArray(entries) ? entries : [])
|
|
32
|
+
.filter(item => METRIC_NAMES.has(item?.name) && Number.isFinite(item?.value))
|
|
33
|
+
.map(item => [item.name, item.value]));
|
|
34
|
+
}
|
|
35
|
+
export async function sampleBrowserDevtools(session, action, durationMs, maxEvents) {
|
|
36
|
+
const events = [];
|
|
37
|
+
const counts = {};
|
|
38
|
+
const requests = new Map();
|
|
39
|
+
function add(event) {
|
|
40
|
+
const key = event.level || event.kind || 'other';
|
|
41
|
+
counts[key] = (counts[key] || 0) + 1;
|
|
42
|
+
if (events.length < maxEvents) events.push(event);
|
|
43
|
+
}
|
|
44
|
+
if (action === 'layout' || action === 'accessibility') return inspectNativeBrowserAudit(session, action);
|
|
45
|
+
if (action === 'geometry') return inspectDomGeometry(session);
|
|
46
|
+
if (action === 'performance') {
|
|
47
|
+
await session.send('Performance.enable');
|
|
48
|
+
try {
|
|
49
|
+
const data = await session.send('Performance.getMetrics');
|
|
50
|
+
return { action, metrics:normalizeDevtoolsMetrics(data.metrics) };
|
|
51
|
+
} finally { await session.send('Performance.disable').catch(() => {}); }
|
|
52
|
+
}
|
|
53
|
+
const unsubscribe = [];
|
|
54
|
+
if (action === 'console') {
|
|
55
|
+
unsubscribe.push(session.observe('Runtime.consoleAPICalled', params => {
|
|
56
|
+
const level = String(params.type || 'log').slice(0, 24);
|
|
57
|
+
const previews = (params.args || []).slice(0, 3).map(arg =>
|
|
58
|
+
arg?.type === 'string' ? arg.value : arg?.description || (arg?.value == null ? arg?.type : String(arg.value)));
|
|
59
|
+
add({ kind:'console', level, message:safeDevtoolsMessage(previews.join(' ')) });
|
|
60
|
+
}));
|
|
61
|
+
unsubscribe.push(session.observe('Runtime.exceptionThrown', params => {
|
|
62
|
+
add({ kind:'exception', level:'error', message:safeDevtoolsMessage(params.exceptionDetails?.text || 'Uncaught exception') });
|
|
63
|
+
}));
|
|
64
|
+
unsubscribe.push(session.observe('Log.entryAdded', params => {
|
|
65
|
+
add({ kind:'log', level:String(params.entry?.level || 'info').slice(0, 24),
|
|
66
|
+
message:safeDevtoolsMessage(params.entry?.text || '') });
|
|
67
|
+
}));
|
|
68
|
+
} else {
|
|
69
|
+
unsubscribe.push(session.observe('Network.requestWillBeSent', params => {
|
|
70
|
+
const id = String(params.requestId || '');
|
|
71
|
+
if (requests.size > 1000) requests.clear();
|
|
72
|
+
const record = { kind:'request', method:String(params.request?.method || 'GET').slice(0, 12),
|
|
73
|
+
resource_type:String(params.type || 'Other').slice(0, 35), url:safeDevtoolsUrl(params.request?.url) };
|
|
74
|
+
requests.set(id,record);
|
|
75
|
+
add(record);
|
|
76
|
+
}));
|
|
77
|
+
unsubscribe.push(session.observe('Network.responseReceived', params => {
|
|
78
|
+
const original = requests.get(String(params.requestId || ''));
|
|
79
|
+
if (!original) return;
|
|
80
|
+
const status = Number(params.response?.status);
|
|
81
|
+
original.status = Number.isFinite(status) ? status : null;
|
|
82
|
+
original.resource_type = String(params.type || original.resource_type).slice(0,35);
|
|
83
|
+
}));
|
|
84
|
+
unsubscribe.push(session.observe('Network.loadingFailed', params => {
|
|
85
|
+
const original = requests.get(String(params.requestId || ''));
|
|
86
|
+
if (original) original.failed = true;
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
if (action === 'console') {
|
|
91
|
+
await session.send('Runtime.enable');
|
|
92
|
+
await session.send('Log.enable');
|
|
93
|
+
} else await session.send('Network.enable', { maxTotalBufferSize: 1_000_000 });
|
|
94
|
+
await new Promise(resolve => setTimeout(resolve, durationMs));
|
|
95
|
+
} finally {
|
|
96
|
+
for (const stop of unsubscribe) stop();
|
|
97
|
+
if (action === 'console') {
|
|
98
|
+
await Promise.allSettled([session.send('Log.disable'), session.send('Runtime.disable')]);
|
|
99
|
+
} else await session.send('Network.disable').catch(() => {});
|
|
100
|
+
}
|
|
101
|
+
return { action, sampled_ms:durationMs, counts, events, truncated:Object.values(counts).reduce((a,b) => a+b,0) > maxEvents,
|
|
102
|
+
note:'Live events only; DevTools does not backfill events recorded before sampling. Request/response contents and headers are excluded.' };
|
|
103
|
+
}
|
|
104
|
+
export function buildDevtoolsReport(target, action, result) {
|
|
105
|
+
return { target_id:target.id,
|
|
106
|
+
...(action === 'geometry' ? {} : {url:safeDevtoolsUrl(target.url)}),
|
|
107
|
+
...result };
|
|
108
|
+
}
|
|
109
|
+
export async function browserDevtools(args = {}) {
|
|
110
|
+
const action = requireEnum(args.action, 'action', ['console','network','performance','layout','accessibility','geometry']);
|
|
111
|
+
const durationMs = clamp(args.sample_ms, 1500, 250, 5000);
|
|
112
|
+
const maxEvents = clamp(args.max_events, 50, 1, 100);
|
|
113
|
+
return withTarget(args, async (session,target) =>
|
|
114
|
+
jsonResult(buildDevtoolsReport(target, action,
|
|
115
|
+
await sampleBrowserDevtools(session,action,durationMs,maxEvents))));
|
|
116
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Local-only Chrome DOMSnapshot geometry aggregation.
|
|
2
|
+
// DOMSnapshot can contain page text and attributes: NEVER serialize, log or return it.
|
|
3
|
+
// The result contains counts and bounded geometry of tag types, not DOM content.
|
|
4
|
+
const ALLOWED_TAGS = new Set([
|
|
5
|
+
'A','ARTICLE','ASIDE','BLOCKQUOTE','BUTTON','CANVAS','CODE','DD','DETAILS','DIV',
|
|
6
|
+
'DL','DT','FIELDSET','FIGCAPTION','FIGURE','FOOTER','FORM','H1','H2','H3','H4',
|
|
7
|
+
'H5','H6','HEADER','IFRAME','IMG','INPUT','LABEL','LI','MAIN','NAV','OL','OPTION',
|
|
8
|
+
'P','PICTURE','PRE','SECTION','SELECT','SPAN','SVG','TABLE','TBODY','TD',
|
|
9
|
+
'TEXTAREA','TH','THEAD','TR','UL','VIDEO',
|
|
10
|
+
]);
|
|
11
|
+
const MAX_LAYOUT_NODES = 12000;
|
|
12
|
+
const MAX_CANDIDATES = 8;
|
|
13
|
+
const EPSILON = 1;
|
|
14
|
+
function decimal(value) {
|
|
15
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
16
|
+
? Math.max(-10000000, Math.min(10000000, Math.round(value * 100) / 100))
|
|
17
|
+
: null;
|
|
18
|
+
}
|
|
19
|
+
function typeForNode(doc, strings, nodeIndex) {
|
|
20
|
+
const idx = doc?.nodes?.nodeName?.[nodeIndex];
|
|
21
|
+
if (!Number.isSafeInteger(idx) || !Array.isArray(strings)) return '';
|
|
22
|
+
const str = strings[idx];
|
|
23
|
+
if (typeof str !== 'string' || str.length > 20) return '';
|
|
24
|
+
const upper = str.toUpperCase();
|
|
25
|
+
return ALLOWED_TAGS.has(upper) ? upper.toLowerCase() : '';
|
|
26
|
+
}
|
|
27
|
+
export function summarizeDomGeometry(snapshot, pageMetrics, { maxNodes = MAX_LAYOUT_NODES } = {}) {
|
|
28
|
+
const rawDocs = Array.isArray(snapshot?.documents) ? snapshot.documents : [];
|
|
29
|
+
const doc = rawDocs[0] || {};
|
|
30
|
+
const indices = Array.isArray(doc?.layout?.nodeIndex) ? doc.layout.nodeIndex : [];
|
|
31
|
+
const bounds = Array.isArray(doc?.layout?.bounds) ? doc.layout.bounds : [];
|
|
32
|
+
const width = decimal(pageMetrics?.cssLayoutViewport?.clientWidth);
|
|
33
|
+
const scrollX = decimal(pageMetrics?.cssLayoutViewport?.pageX) ?? 0;
|
|
34
|
+
if (width == null || width <= 0) throw new Error('Chrome did not report a valid layout viewport width');
|
|
35
|
+
const left = scrollX, right = scrollX + width;
|
|
36
|
+
const count = Math.min(MAX_LAYOUT_NODES, Math.max(0,Math.floor(Number(maxNodes) || MAX_LAYOUT_NODES)),indices.length,bounds.length);
|
|
37
|
+
let eligible = 0, candidateCount = 0, leftOverflowCount = 0, rightOverflowCount = 0;
|
|
38
|
+
let biggestOverflow = 0;
|
|
39
|
+
const byTag = Object.create(null);
|
|
40
|
+
const top = [];
|
|
41
|
+
for (let i = 0; i < count; i++) {
|
|
42
|
+
const tag = typeForNode(doc,snapshot?.strings,indices[i]);
|
|
43
|
+
if (!tag) continue;
|
|
44
|
+
const box = bounds[i];
|
|
45
|
+
if (!Array.isArray(box) || box.length < 4) continue;
|
|
46
|
+
const [x,y,w,h] = box.map(decimal);
|
|
47
|
+
if ([x,y,w,h].some(v => v == null) || w <= 0 || h <= 0) continue;
|
|
48
|
+
eligible++;
|
|
49
|
+
const extraLeft = Math.max(0,left-x);
|
|
50
|
+
const extraRight = Math.max(0,x+w-right);
|
|
51
|
+
if (extraLeft <= EPSILON && extraRight <= EPSILON) continue;
|
|
52
|
+
candidateCount++;
|
|
53
|
+
if (extraLeft > EPSILON) leftOverflowCount++;
|
|
54
|
+
if (extraRight > EPSILON) rightOverflowCount++;
|
|
55
|
+
const overflow = Math.ceil(Math.max(extraLeft,extraRight));
|
|
56
|
+
biggestOverflow = Math.max(biggestOverflow,overflow);
|
|
57
|
+
byTag[tag] = (byTag[tag] || 0) + 1;
|
|
58
|
+
// Never send selectors, attributes, text, sources, titles or URLs.
|
|
59
|
+
const candidate = { tag, side:extraLeft > EPSILON && extraRight > EPSILON ? 'both' :
|
|
60
|
+
extraLeft > EPSILON ? 'left' : 'right', overflow_px:overflow,
|
|
61
|
+
bounds: { x, y, width:w, height:h } };
|
|
62
|
+
if (top.length < MAX_CANDIDATES) top.push(candidate);
|
|
63
|
+
else {
|
|
64
|
+
let smallest = 0;
|
|
65
|
+
for (let j=1; j<top.length; j++) if (top[j].overflow_px < top[smallest].overflow_px) smallest = j;
|
|
66
|
+
if (candidate.overflow_px > top[smallest].overflow_px) top[smallest] = candidate;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
top.sort((a,b)=>b.overflow_px-a.overflow_px);
|
|
70
|
+
return {
|
|
71
|
+
viewport_width_px:width,
|
|
72
|
+
sampled_layout_nodes:count,
|
|
73
|
+
eligible_elements:eligible,
|
|
74
|
+
candidate_count:candidateCount,
|
|
75
|
+
left_overflow_count:leftOverflowCount,
|
|
76
|
+
right_overflow_count:rightOverflowCount,
|
|
77
|
+
maximum_overflow_px:biggestOverflow,
|
|
78
|
+
by_tag:Object.fromEntries(Object.entries(byTag).sort((a,b)=>b[1]-a[1]).slice(0,20)),
|
|
79
|
+
candidates:top,
|
|
80
|
+
frames_omitted:Math.max(0,rawDocs.length-1),
|
|
81
|
+
truncated:count<indices.length || count<bounds.length,
|
|
82
|
+
note:'Only numeric geometry and allowlisted HTML tag types leave the device. Candidate bounds may be intentional (for example carousels or offscreen animations); this is not proof of a layout bug. Only the main frame is inspected.',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export async function inspectDomGeometry(session) {
|
|
86
|
+
// Fetch just enough geometry for local assessment. The CDP transport enforces
|
|
87
|
+
// its own 16 MiB message bound; snapshot text never leaves this process.
|
|
88
|
+
const [metrics,snapshot] = await Promise.all([
|
|
89
|
+
session.send('Page.getLayoutMetrics'),
|
|
90
|
+
session.send('DOMSnapshot.captureSnapshot',{
|
|
91
|
+
computedStyles:[],includePaintOrder:false,includeDOMRects:false,
|
|
92
|
+
}),
|
|
93
|
+
]);
|
|
94
|
+
return {action:'geometry',geometry:summarizeDomGeometry(snapshot,metrics)};
|
|
95
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Read-only, native CDP audits. Only numeric aggregates leave the device.
|
|
2
|
+
// Unlike DOM snapshots, these methods do not transmit page text to ReMCP.
|
|
3
|
+
const CONTROL_ROLES = new Set([
|
|
4
|
+
'button','link','checkbox','radio','switch','textbox','searchbox','combobox',
|
|
5
|
+
'menuitem','menuitemcheckbox','menuitemradio','tab','slider','spinbutton',
|
|
6
|
+
]);
|
|
7
|
+
const REPORT_ROLES = new Set([
|
|
8
|
+
...CONTROL_ROLES, 'heading','image','img','form','table','dialog','alert',
|
|
9
|
+
'navigation','main','region','list','listitem','row','cell',
|
|
10
|
+
]);
|
|
11
|
+
const finite = value => typeof value === 'number' && Number.isFinite(value);
|
|
12
|
+
const positive = value => finite(value) ? Math.max(0,Math.min(10_000_000, Math.round(value * 100) / 100)) : 0;
|
|
13
|
+
|
|
14
|
+
export function normalizeLayoutMetrics(result = {}) {
|
|
15
|
+
const viewport = result.cssLayoutViewport || {};
|
|
16
|
+
const visual = result.cssVisualViewport || {};
|
|
17
|
+
const content = result.cssContentSize || {};
|
|
18
|
+
const clientWidth = positive(viewport.clientWidth);
|
|
19
|
+
const clientHeight = positive(viewport.clientHeight);
|
|
20
|
+
const contentWidth = positive(content.width);
|
|
21
|
+
const contentHeight = positive(content.height);
|
|
22
|
+
return {
|
|
23
|
+
viewport: { width:clientWidth, height:clientHeight, page_x:positive(viewport.pageX), page_y:positive(viewport.pageY) },
|
|
24
|
+
visual_viewport: {
|
|
25
|
+
width:positive(visual.clientWidth), height:positive(visual.clientHeight),
|
|
26
|
+
scale:finite(visual.scale) ? Math.max(0,Math.min(100,visual.scale)) : null,
|
|
27
|
+
},
|
|
28
|
+
document: { width:contentWidth, height:contentHeight },
|
|
29
|
+
horizontal_overflow_px:Math.max(0,Math.ceil(contentWidth-clientWidth)),
|
|
30
|
+
vertical_scroll_px:Math.max(0,Math.ceil(contentHeight-clientHeight)),
|
|
31
|
+
note:'Native CSS-pixel layout metrics from the current page. Overflow measures the document bounds, not whether a particular element is visually clipped.',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function summarizeAccessibilityTree(nodes, { depth = 6 } = {}) {
|
|
35
|
+
const bounded = (Array.isArray(nodes) ? nodes : []).slice(0, 5000);
|
|
36
|
+
const roles = {};
|
|
37
|
+
let ignored = 0, interactive = 0, unlabeledControls = 0, unnamedImages = 0;
|
|
38
|
+
let headings = 0;
|
|
39
|
+
const headingLevels = {};
|
|
40
|
+
for (const node of bounded) {
|
|
41
|
+
if (!node || typeof node !== 'object') continue;
|
|
42
|
+
if (node.ignored === true) { ignored++; continue; }
|
|
43
|
+
const role = String(node.role?.value || '').toLowerCase();
|
|
44
|
+
if (!REPORT_ROLES.has(role)) continue;
|
|
45
|
+
roles[role] = (roles[role] || 0) + 1;
|
|
46
|
+
const name = String(node.name?.value || '').trim();
|
|
47
|
+
if (CONTROL_ROLES.has(role)) {
|
|
48
|
+
interactive++;
|
|
49
|
+
// Anonymous count only: actual labels, element identifiers and URLs are never returned.
|
|
50
|
+
if (!name) unlabeledControls++;
|
|
51
|
+
}
|
|
52
|
+
if (role === 'image' || role === 'img') {
|
|
53
|
+
if (!name) unnamedImages++;
|
|
54
|
+
}
|
|
55
|
+
if (role === 'heading') {
|
|
56
|
+
headings++;
|
|
57
|
+
const level = node.properties?.find(item => item?.name === 'level')?.value?.value;
|
|
58
|
+
if (Number.isSafeInteger(level) && level >= 1 && level <= 6)
|
|
59
|
+
headingLevels[level] = (headingLevels[level] || 0) + 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
sampled_nodes:bounded.length, ignored_nodes:ignored, requested_depth:depth,
|
|
64
|
+
roles, interactive_controls:interactive, unlabeled_controls:unlabeledControls,
|
|
65
|
+
headings, heading_levels:headingLevels, unnamed_images:unnamedImages,
|
|
66
|
+
truncated: Array.isArray(nodes) && nodes.length > bounded.length,
|
|
67
|
+
note:'Bounded native accessibility tree sample. These are diagnostic candidates, not a WCAG compliance verdict. Nodes deeper than the requested depth are not inspected.',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export async function inspectNativeBrowserAudit(session, action) {
|
|
71
|
+
if (action === 'layout') {
|
|
72
|
+
const result = await session.send('Page.getLayoutMetrics');
|
|
73
|
+
return { action, layout:normalizeLayoutMetrics(result) };
|
|
74
|
+
}
|
|
75
|
+
if (action === 'accessibility') {
|
|
76
|
+
const depth = 6;
|
|
77
|
+
const result = await session.send('Accessibility.getFullAXTree',{depth});
|
|
78
|
+
return { action, accessibility:summarizeAccessibilityTree(result.nodes,{depth}) };
|
|
79
|
+
}
|
|
80
|
+
throw new Error('Unsupported native browser audit mode');
|
|
81
|
+
}
|
package/src/extended/catalog.mjs
CHANGED
|
@@ -167,6 +167,7 @@ export const extendedToolDefinitions = [
|
|
|
167
167
|
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() }), readOnlyOpen, browserHandlers.browser_find, ['browser_cdp']),
|
|
168
168
|
define('browser_action', 'Browser element action', 'Click, focus, type/set a value, fill up to 20 form fields in one guarded call, select, scroll a page or scrollable element, scroll an element into view, 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','scroll_into_view','upload','press','set_viewport','fill_form','workflow']), selector:s('CSS selector for the target element; prefer the reusable selector returned by browser_find.'), steps:{type:'array',minItems:1,maxItems:12,description:'Workflow on one page: explicit CSS actions and bounded waits, max 12 seconds. Never silently submits; click must be explicit.',items:{type:'object',additionalProperties:false,properties:{action:e(['fill_form','set_value','select','focus','click','wait_for']),selector:s('CSS selector for action or wait_for.'),value:{type:['string','number','boolean']},fields:{type:'array',description:'CSS-selected field edits within the workflow step; no implicit form submission.',minItems:1,maxItems:20,items:{type:'object',additionalProperties:false,properties:{selector:s(),value:{type:['string','number','boolean']}},required:['selector','value']}},condition:e(['selector','text','url_contains','load']),text:s('Text or URL fragment for wait condition.'),timeout_ms:n('Wait timeout 100..4000 ms; default 1500.')},required:['action']}}, fields:{type:'array',minItems:1,maxItems:20,items:{type:'object',additionalProperties:false,properties:{selector:s('CSS selector.'),value:{type:['string','boolean','number'],description:'New field value; not returned in results.'}},required:['selector','value']},description:'fill_form: 1–20 CSS form fields in one CDP call. Does not submit.'}, browser_text:s('Visible text used only to locate a DOM element; use this instead of overloading text for action=type.'), text:s('For action=type, Unicode text to insert. For non-type element actions without selector/browser_text, visible text may identify the target.'), clear:b('For action=type, clear the current value before inserting text.'), value:s('Value for action=set_value; also accepted as a legacy type payload when text is used as the locator.'), text_value:s('Compatibility alias for the type/set_value text value.'), option:s(), delta_x:n('Horizontal scroll delta; positive means right.'), delta_y:n('Vertical scroll 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 for direction; 1..50.'), 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']),
|
|
169
169
|
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.') }), readOnlyOpen, browserHandlers.browser_wait, ['browser_cdp']),
|
|
170
|
+
define('browser_devtools', 'Browser DevTools diagnostics', 'Use this to inspect Chrome Console/Network live events, Performance, Layout, Accessibility or screenshot-free DOMSnapshot Geometry via native CDP. Network headers, bodies and cookies are excluded; geometry returns bounded numeric summaries without page text.', o({ ...browserTarget, action:e(['console','network','performance','layout','accessibility','geometry'],'Diagnostics mode. Layout measures CSS viewport, accessibility returns anonymized roles and unnamed controls, geometry audits DOMSnapshot bounds without page contents.'), sample_ms:n('Live event capture period 250–5000 ms; default 1500. Ignored for performance, layout, accessibility and geometry.'), max_events:n('Maximum 1–100 events retained; default 50.') }, ['action']), readOnlyOpen, browserHandlers.browser_devtools, ['browser_cdp']),
|
|
170
171
|
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_evaluate']),
|
|
171
172
|
|
|
172
173
|
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']),
|