@remcp/runtime 0.2.57 → 0.2.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/extended/desktop.mjs +111 -38
- package/src/mcp-v2.mjs +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.2.58 — 2026-09-20
|
|
6
|
+
|
|
7
|
+
- `computer_snapshot` normalizes partial platform-capture failures into schema-stable collections/objects and bounded `{ source, message }` errors instead of leaking error strings into structured output.
|
|
8
|
+
- Output-budget pruning preserves the structured snapshot error contract, and browser snapshot fallbacks reject invalid/error-shaped payloads.
|
|
9
|
+
- Dynamic browser/CDP capability groups now toggle registrations in one batch and emit one `tools/list_changed` notification per transition, preventing concurrent stdio notification writes from exceeding Node's listener limit.
|
|
10
|
+
- Lockstep patch release with the ReMCP client at `0.2.58`.
|
|
11
|
+
|
|
5
12
|
## 0.2.57 — 2026-09-20
|
|
6
13
|
|
|
7
14
|
- ZIP extraction recognizes Info-ZIP `unzip` on Debian/Ubuntu by probing it with `-v`, fixing the false “unzip is not installed” error reproduced during physical 76-tool package verification.
|
package/package.json
CHANGED
package/src/extended/desktop.mjs
CHANGED
|
@@ -137,35 +137,78 @@ function resultValue(result) {
|
|
|
137
137
|
try { return JSON.parse(raw); } catch { return raw; }
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
function objectRecord(value) {
|
|
141
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function toolResultMessage(result) {
|
|
145
|
+
const text = result?.content?.find(part => part.type === 'text')?.text;
|
|
146
|
+
return String(text || 'tool returned an error result').slice(0, 2000);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function snapshotError(source, settled) {
|
|
150
|
+
if (settled?.status === 'rejected') {
|
|
151
|
+
return { source, message:String(settled.reason?.message || settled.reason || 'capture rejected').slice(0, 2000) };
|
|
152
|
+
}
|
|
153
|
+
if (settled?.status === 'fulfilled' && settled.value?.isError === true) {
|
|
154
|
+
return { source, message:toolResultMessage(settled.value) };
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function normalizeSnapshotErrors(errors) {
|
|
160
|
+
if (!Array.isArray(errors)) return [];
|
|
161
|
+
return errors.slice(0, 50).map((error, index) => {
|
|
162
|
+
if (objectRecord(error)) {
|
|
163
|
+
return {
|
|
164
|
+
...error,
|
|
165
|
+
source:String(error.source || 'snapshot').slice(0, 120),
|
|
166
|
+
message:String(error.message || error.error || 'snapshot capture error').slice(0, 2000),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return { source:'snapshot', message:String(error ?? `snapshot error ${index + 1}`).slice(0, 2000) };
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
140
173
|
function jsonBytes(value) {
|
|
141
174
|
return Buffer.byteLength(JSON.stringify(value), 'utf8');
|
|
142
175
|
}
|
|
143
176
|
|
|
144
177
|
export function boundedComputerSnapshot(payload) {
|
|
178
|
+
const normalized = {
|
|
179
|
+
...payload,
|
|
180
|
+
active_window:objectRecord(payload?.active_window),
|
|
181
|
+
windows:Array.isArray(payload?.windows) ? payload.windows : [],
|
|
182
|
+
displays:Array.isArray(payload?.displays) ? payload.displays : [],
|
|
183
|
+
cursor:objectRecord(payload?.cursor) || { x:null, y:null },
|
|
184
|
+
ui:objectRecord(payload?.ui) || { platform:process.platform, count:0, nodes:[] },
|
|
185
|
+
clipboard:objectRecord(payload?.clipboard) || { available:false, length:0 },
|
|
186
|
+
errors:normalizeSnapshotErrors(payload?.errors),
|
|
187
|
+
};
|
|
145
188
|
const budget = Math.min(768 * 1024, Math.max(16 * 1024, Math.floor(Number(liveConfig('maxOutputBytes') || 0) * 0.35)));
|
|
146
|
-
const originalBytes = jsonBytes(
|
|
189
|
+
const originalBytes = jsonBytes(normalized);
|
|
147
190
|
if (originalBytes <= budget) {
|
|
148
|
-
return { ...
|
|
191
|
+
return { ...normalized, snapshot_bytes:originalBytes, snapshot_budget_bytes:budget, snapshot_truncated:false };
|
|
149
192
|
}
|
|
150
193
|
|
|
151
194
|
const bounded = {
|
|
152
|
-
...
|
|
153
|
-
windows:
|
|
154
|
-
errors:
|
|
155
|
-
ui:
|
|
156
|
-
...
|
|
157
|
-
...(Array.isArray(
|
|
158
|
-
}
|
|
159
|
-
browser:
|
|
160
|
-
...
|
|
161
|
-
...(Array.isArray(
|
|
162
|
-
} :
|
|
163
|
-
ocr:
|
|
164
|
-
...
|
|
165
|
-
text:String(
|
|
166
|
-
...(Array.isArray(
|
|
167
|
-
...(Array.isArray(
|
|
168
|
-
} :
|
|
195
|
+
...normalized,
|
|
196
|
+
windows:[...normalized.windows],
|
|
197
|
+
errors:normalizeSnapshotErrors(normalized.errors),
|
|
198
|
+
ui:{
|
|
199
|
+
...normalized.ui,
|
|
200
|
+
...(Array.isArray(normalized.ui.nodes) ? { nodes:[...normalized.ui.nodes] } : {}),
|
|
201
|
+
},
|
|
202
|
+
browser:objectRecord(normalized.browser) ? {
|
|
203
|
+
...normalized.browser,
|
|
204
|
+
...(Array.isArray(normalized.browser.nodes) ? { nodes:[...normalized.browser.nodes] } : {}),
|
|
205
|
+
} : undefined,
|
|
206
|
+
ocr:objectRecord(normalized.ocr) ? {
|
|
207
|
+
...normalized.ocr,
|
|
208
|
+
text:String(normalized.ocr.text || '').slice(0, 50_000),
|
|
209
|
+
...(Array.isArray(normalized.ocr.words) ? { words:[...normalized.ocr.words] } : {}),
|
|
210
|
+
...(Array.isArray(normalized.ocr.lines) ? { lines:[...normalized.ocr.lines] } : {}),
|
|
211
|
+
} : undefined,
|
|
169
212
|
snapshot_original_bytes:originalBytes,
|
|
170
213
|
snapshot_budget_bytes:budget,
|
|
171
214
|
snapshot_truncated:true,
|
|
@@ -939,15 +982,33 @@ export async function computerSnapshot(args = {}) {
|
|
|
939
982
|
cursorPosition(),
|
|
940
983
|
captureScreenshot ? computerScreenshotTask(args) : Promise.resolve(null),
|
|
941
984
|
]);
|
|
942
|
-
const parse = result => {
|
|
943
|
-
if (result.status !== 'fulfilled' || !result.value) return
|
|
944
|
-
|
|
945
|
-
if (typeof raw !== 'string') return null;
|
|
946
|
-
try { return JSON.parse(raw); } catch { return raw; }
|
|
985
|
+
const parse = (result, fallback = null) => {
|
|
986
|
+
if (result.status !== 'fulfilled' || !result.value || result.value.isError === true) return fallback;
|
|
987
|
+
return resultValue(result.value) ?? fallback;
|
|
947
988
|
};
|
|
948
|
-
const
|
|
949
|
-
const
|
|
950
|
-
const
|
|
989
|
+
const shapeErrors = [];
|
|
990
|
+
const parsedWindows = parse(windowsResult, []);
|
|
991
|
+
const windows = Array.isArray(parsedWindows) ? parsedWindows : [];
|
|
992
|
+
if (parsedWindows !== windows && parsedWindows != null) {
|
|
993
|
+
shapeErrors.push({ source:'windows', message:'window inventory returned an invalid result shape' });
|
|
994
|
+
}
|
|
995
|
+
const parsedDisplays = parse(displaysResult, []);
|
|
996
|
+
const displays = Array.isArray(parsedDisplays) ? parsedDisplays : [];
|
|
997
|
+
if (parsedDisplays !== displays && parsedDisplays != null) {
|
|
998
|
+
shapeErrors.push({ source:'displays', message:'display inventory returned an invalid result shape' });
|
|
999
|
+
}
|
|
1000
|
+
const parsedCursor = parse(cursorResult, { x:null, y:null });
|
|
1001
|
+
const cursor = objectRecord(parsedCursor) || { x:null, y:null };
|
|
1002
|
+
if (parsedCursor !== cursor && parsedCursor != null) {
|
|
1003
|
+
shapeErrors.push({ source:'cursor', message:'cursor position returned an invalid result shape' });
|
|
1004
|
+
}
|
|
1005
|
+
const parsedUi = includeUi ? parse(uiResult, null) : null;
|
|
1006
|
+
const ui = objectRecord(parsedUi) || { platform:process.platform, count:0, nodes:[], ...(includeUi ? { unavailable:true } : { skipped:true }) };
|
|
1007
|
+
if (includeUi && parsedUi !== ui && parsedUi != null) {
|
|
1008
|
+
shapeErrors.push({ source:'ui', message:'accessibility snapshot returned an invalid result shape' });
|
|
1009
|
+
}
|
|
1010
|
+
const clipboardOk = clipboardResult.status === 'fulfilled' && clipboardResult.value?.isError !== true;
|
|
1011
|
+
const clipText = clipboardOk ? clipboardResult.value.content?.[0]?.text || '' : '';
|
|
951
1012
|
const uiNodes = Array.isArray(ui?.nodes) ? ui.nodes : Array.isArray(ui) ? ui : [];
|
|
952
1013
|
const focusedNodes = uiNodes.filter(node => node.focused === true);
|
|
953
1014
|
const focusScore = node => {
|
|
@@ -1002,18 +1063,24 @@ export async function computerSnapshot(args = {}) {
|
|
|
1002
1063
|
if (browserRequested && await browserCapabilityAvailable(undefined, 400)) {
|
|
1003
1064
|
const hint = browserTitleHint(activeWindow);
|
|
1004
1065
|
try {
|
|
1005
|
-
|
|
1066
|
+
const browserResult = await browserSnapshot({
|
|
1006
1067
|
...(hint ? { title:hint } : {}),
|
|
1007
1068
|
max_nodes:clamp(args.browser_max_nodes, 500, 1, 2000),
|
|
1008
1069
|
timeout_ms:3000,
|
|
1009
|
-
})
|
|
1070
|
+
});
|
|
1071
|
+
if (browserResult?.isError === true) throw new Error(toolResultMessage(browserResult));
|
|
1072
|
+
browser = objectRecord(resultValue(browserResult));
|
|
1073
|
+
if (!browser) throw new Error('browser snapshot returned an invalid result shape');
|
|
1010
1074
|
} catch (error) {
|
|
1011
1075
|
if (hint) {
|
|
1012
1076
|
try {
|
|
1013
|
-
|
|
1077
|
+
const fallbackResult = await browserSnapshot({
|
|
1014
1078
|
max_nodes:clamp(args.browser_max_nodes, 500, 1, 2000),
|
|
1015
1079
|
timeout_ms:3000,
|
|
1016
|
-
})
|
|
1080
|
+
});
|
|
1081
|
+
if (fallbackResult?.isError === true) throw new Error(toolResultMessage(fallbackResult));
|
|
1082
|
+
browser = objectRecord(resultValue(fallbackResult));
|
|
1083
|
+
if (!browser) throw new Error('browser snapshot returned an invalid result shape');
|
|
1017
1084
|
} catch (fallbackError) {
|
|
1018
1085
|
browserError = String(fallbackError?.message || fallbackError);
|
|
1019
1086
|
}
|
|
@@ -1058,8 +1125,8 @@ export async function computerSnapshot(args = {}) {
|
|
|
1058
1125
|
active_app: activeApp,
|
|
1059
1126
|
active_window: activeWindow,
|
|
1060
1127
|
windows,
|
|
1061
|
-
displays
|
|
1062
|
-
cursor
|
|
1128
|
+
displays,
|
|
1129
|
+
cursor,
|
|
1063
1130
|
ui,
|
|
1064
1131
|
...(browser ? { browser } : {}),
|
|
1065
1132
|
...(ocr ? { ocr } : {}),
|
|
@@ -1098,12 +1165,18 @@ export async function computerSnapshot(args = {}) {
|
|
|
1098
1165
|
: (args.screenshot_monitor != null || args.screenshot_monitor_index != null) ? 'monitor'
|
|
1099
1166
|
: 'desktop',
|
|
1100
1167
|
},
|
|
1101
|
-
clipboard: { available:
|
|
1168
|
+
clipboard: { available:clipboardOk, length:clipText.length },
|
|
1102
1169
|
errors: [
|
|
1103
|
-
...[
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1170
|
+
...[
|
|
1171
|
+
['windows', windowsResult],
|
|
1172
|
+
['displays', displaysResult],
|
|
1173
|
+
...(includeUi ? [['ui', uiResult]] : []),
|
|
1174
|
+
['clipboard', clipboardResult],
|
|
1175
|
+
['cursor', cursorResult],
|
|
1176
|
+
...(captureScreenshot ? [['screenshot', shotResult]] : []),
|
|
1177
|
+
].map(([source, result]) => snapshotError(source, result)).filter(Boolean),
|
|
1178
|
+
...shapeErrors,
|
|
1179
|
+
...(browserError ? [{ source:'browser', message:browserError.slice(0, 2000) }] : []),
|
|
1107
1180
|
],
|
|
1108
1181
|
};
|
|
1109
1182
|
const boundedPayload = boundedComputerSnapshot(payload);
|
package/src/mcp-v2.mjs
CHANGED
|
@@ -64,15 +64,23 @@ export async function startRuntimeMcpServer({ version, instructions, onError = (
|
|
|
64
64
|
if (closed) return;
|
|
65
65
|
try {
|
|
66
66
|
const next = new Set((await advertisedExtendedTools()).map(tool => tool.name));
|
|
67
|
+
let changed = false;
|
|
67
68
|
for (const definition of extendedToolDefinitions) {
|
|
68
69
|
const before = lastSupported.has(definition.name);
|
|
69
70
|
const after = next.has(definition.name);
|
|
70
71
|
if (before === after) continue;
|
|
71
72
|
const registered = registrations.get(definition.name);
|
|
72
|
-
if (
|
|
73
|
-
|
|
73
|
+
if (!registered) continue;
|
|
74
|
+
// The SDK's enable()/disable() helper emits tools/list_changed immediately. Browser CDP
|
|
75
|
+
// appears as a seven-tool capability group, so calling those helpers in a loop creates
|
|
76
|
+
// seven concurrent stdio writes and can exceed Node's listener limit. Mutating the
|
|
77
|
+
// registration handle is otherwise identical to update({enabled}), then one notification
|
|
78
|
+
// below publishes the complete new toolset atomically.
|
|
79
|
+
registered.enabled = after;
|
|
80
|
+
changed = true;
|
|
74
81
|
}
|
|
75
82
|
lastSupported = next;
|
|
83
|
+
if (changed) server.sendToolListChanged();
|
|
76
84
|
} catch (error) {
|
|
77
85
|
onError(error instanceof Error ? error : new Error(String(error)));
|
|
78
86
|
}
|