@remcp/runtime 0.2.47 → 0.2.49
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 +23 -0
- package/README.md +45 -26
- package/package.json +6 -2
- package/src/catalog.mjs +73 -1
- package/src/extended/browser.mjs +592 -0
- package/src/extended/catalog.mjs +564 -0
- package/src/extended/common.mjs +209 -0
- package/src/extended/desktop-linux.mjs +1173 -0
- package/src/extended/desktop-macos.mjs +408 -0
- package/src/extended/desktop-windows.mjs +546 -0
- package/src/extended/desktop.mjs +1313 -0
- package/src/extended/diagnostics.mjs +274 -0
- package/src/extended/documents.mjs +381 -0
- package/src/extended/ocr.mjs +139 -0
- package/src/helpers/wayland-eis-helper.py +450 -0
- package/src/index.mjs +46 -44
- package/src/invoke.mjs +3 -3
- package/src/mcp-v2.mjs +104 -0
- package/src/permissions.mjs +1 -1
- package/src/tools/terminal.mjs +58 -13
- package/src/util.mjs +24 -1
- package/src/wayland-remote-desktop.mjs +654 -0
package/src/mcp-v2.mjs
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { toolDefinitions, TEXT_OUTPUT_SCHEMA } from './catalog.mjs';
|
|
3
|
+
import { advertisedExtendedTools, extendedToolDefinitions } from './extended/catalog.mjs';
|
|
4
|
+
import { invokeTool } from './invoke.mjs';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CAPABILITY_POLL_MS = 3000;
|
|
7
|
+
|
|
8
|
+
function pollIntervalMs() {
|
|
9
|
+
const requested = Number(process.env.REMCP_CAPABILITY_POLL_MS);
|
|
10
|
+
return Math.min(60_000, Math.max(250, Number.isFinite(requested) && requested > 0 ? requested : DEFAULT_CAPABILITY_POLL_MS));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function toolConfig(definition, fromJsonSchema) {
|
|
14
|
+
return {
|
|
15
|
+
title: definition.title,
|
|
16
|
+
description: definition.description,
|
|
17
|
+
inputSchema: fromJsonSchema(definition.inputSchema || { type: 'object' }),
|
|
18
|
+
outputSchema: fromJsonSchema(definition.outputSchema || TEXT_OUTPUT_SCHEMA),
|
|
19
|
+
annotations: definition.annotations,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function startRuntimeMcpServer({ version, instructions, onError = () => {} } = {}) {
|
|
24
|
+
const [{ McpServer, fromJsonSchema }, { serveStdio }] = await Promise.all([
|
|
25
|
+
import('@modelcontextprotocol/server'),
|
|
26
|
+
import('@modelcontextprotocol/server/stdio'),
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const initiallySupported = new Set((await advertisedExtendedTools()).map(tool => tool.name));
|
|
30
|
+
let activeServer = null;
|
|
31
|
+
let capabilityTimer = null;
|
|
32
|
+
let closed = false;
|
|
33
|
+
|
|
34
|
+
const serverHandle = serveStdio(() => {
|
|
35
|
+
const server = new McpServer(
|
|
36
|
+
{ name: 'remcp-runtime', version },
|
|
37
|
+
{
|
|
38
|
+
capabilities: {
|
|
39
|
+
tools: { listChanged: true },
|
|
40
|
+
experimental: { 'remcp/telemetry': { version: 1, optOut: true } },
|
|
41
|
+
},
|
|
42
|
+
instructions,
|
|
43
|
+
cacheHints: {
|
|
44
|
+
'tools/list': { ttlMs: 1500, cacheScope: 'private' },
|
|
45
|
+
'server/discover': { ttlMs: 3000, cacheScope: 'private' },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
activeServer = server;
|
|
50
|
+
|
|
51
|
+
const registrations = new Map();
|
|
52
|
+
for (const definition of [...toolDefinitions, ...extendedToolDefinitions]) {
|
|
53
|
+
const registered = server.registerTool(
|
|
54
|
+
definition.name,
|
|
55
|
+
toolConfig(definition, fromJsonSchema),
|
|
56
|
+
async (args, context) => invokeTool(definition.name, args || {}, { signal: context?.mcpReq?.signal }),
|
|
57
|
+
);
|
|
58
|
+
registrations.set(definition.name, registered);
|
|
59
|
+
if (extendedToolDefinitions.includes(definition) && !initiallySupported.has(definition.name)) registered.disable();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let lastSupported = initiallySupported;
|
|
63
|
+
capabilityTimer = setInterval(async () => {
|
|
64
|
+
if (closed) return;
|
|
65
|
+
try {
|
|
66
|
+
const next = new Set((await advertisedExtendedTools()).map(tool => tool.name));
|
|
67
|
+
for (const definition of extendedToolDefinitions) {
|
|
68
|
+
const before = lastSupported.has(definition.name);
|
|
69
|
+
const after = next.has(definition.name);
|
|
70
|
+
if (before === after) continue;
|
|
71
|
+
const registered = registrations.get(definition.name);
|
|
72
|
+
if (after) registered?.enable();
|
|
73
|
+
else registered?.disable();
|
|
74
|
+
}
|
|
75
|
+
lastSupported = next;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
onError(error instanceof Error ? error : new Error(String(error)));
|
|
78
|
+
}
|
|
79
|
+
}, pollIntervalMs());
|
|
80
|
+
capabilityTimer.unref?.();
|
|
81
|
+
|
|
82
|
+
return server;
|
|
83
|
+
}, {
|
|
84
|
+
legacy: 'serve',
|
|
85
|
+
onerror(error) {
|
|
86
|
+
onError(error instanceof Error ? error : new Error(String(error)));
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
async notification(notification) {
|
|
92
|
+
if (!activeServer) return;
|
|
93
|
+
await activeServer.server.notification(notification);
|
|
94
|
+
},
|
|
95
|
+
async close() {
|
|
96
|
+
if (closed) return;
|
|
97
|
+
closed = true;
|
|
98
|
+
if (capabilityTimer) clearInterval(capabilityTimer);
|
|
99
|
+
capabilityTimer = null;
|
|
100
|
+
await serverHandle.close();
|
|
101
|
+
activeServer = null;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
package/src/permissions.mjs
CHANGED
|
@@ -75,7 +75,7 @@ export function filesystemErrorExplanation(error, options = {}) {
|
|
|
75
75
|
return `Windows Controlled folder access is blocking ${windowsLocation} (${code}). Add the binary that runs the tools (${execPath}) under Windows Security → Virus & threat protection → Ransomware protection → Allow an app through Controlled folder access, then restart the agent with \`remcp start\`. A folder outside Desktop, Documents, Downloads and OneDrive needs no new permission.`;
|
|
76
76
|
}
|
|
77
77
|
if (platform === 'darwin') {
|
|
78
|
-
return `macOS denied access to ${where} (${code}). Check the
|
|
78
|
+
return `macOS denied access to ${where} (${code}). Check that this user owns the folder and its parents, or choose another path. If normal permissions are correct and macOS privacy still blocks it, add ${execPath} to System Settings → Privacy & Security → Full Disk Access and restart the agent with \`remcp start\`.`;
|
|
79
79
|
}
|
|
80
80
|
if (platform === 'win32') {
|
|
81
81
|
return `Windows denied access to ${where} (${code}). Check whether the file is read-only, open in another program, or inside a Controlled folder access area, then allow ${execPath} through Windows Security (Controlled folder access) and restart the agent with \`remcp start\`.`;
|
package/src/tools/terminal.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
waitForProcessExit,
|
|
18
18
|
waitForProcessActivity,
|
|
19
19
|
} from '../sessions.mjs';
|
|
20
|
-
import { clampInteger, fail, requireInteger, requireString, text } from '../util.mjs';
|
|
20
|
+
import { clampInteger, fail, requireInteger, requireString, structured, text } from '../util.mjs';
|
|
21
21
|
|
|
22
22
|
function shellCommand() {
|
|
23
23
|
if (runtimeConfig.defaultShell) return runtimeConfig.defaultShell;
|
|
@@ -32,7 +32,15 @@ function shellArgs(command) {
|
|
|
32
32
|
|
|
33
33
|
function describeSession(session) {
|
|
34
34
|
const status = session.exited ? `exited${session.signal ? ` (${session.signal})` : session.exitCode === null ? '' : ` (code ${session.exitCode})`}` : 'running';
|
|
35
|
-
return {
|
|
35
|
+
return {
|
|
36
|
+
pid: session.pid,
|
|
37
|
+
status,
|
|
38
|
+
runtimeMs: (session.finishedAt || Date.now()) - session.startedAt,
|
|
39
|
+
lines: totalLines(session),
|
|
40
|
+
exited: Boolean(session.exited),
|
|
41
|
+
exitCode: session.exitCode ?? null,
|
|
42
|
+
signal: session.signal ?? null,
|
|
43
|
+
};
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
function abortError() {
|
|
@@ -90,8 +98,16 @@ export async function startProcessTool(args, extra = {}) {
|
|
|
90
98
|
const partial = session.partial;
|
|
91
99
|
session.cursor = session.droppedLines + session.lines.length;
|
|
92
100
|
session.lastPartialRead = session.partial || null;
|
|
93
|
-
const warning = verdict.note ?
|
|
94
|
-
|
|
101
|
+
const warning = verdict.note ? String(verdict.note) : '';
|
|
102
|
+
const rendered = [warning, headline, output, partial].filter(Boolean).join('\n');
|
|
103
|
+
const status = describeSession(session);
|
|
104
|
+
return structured({
|
|
105
|
+
...status,
|
|
106
|
+
command,
|
|
107
|
+
output,
|
|
108
|
+
...(partial ? { partial } : {}),
|
|
109
|
+
...(warning ? { warning } : {}),
|
|
110
|
+
}, rendered);
|
|
95
111
|
}
|
|
96
112
|
|
|
97
113
|
export async function readProcessOutputTool(args, extra = {}) {
|
|
@@ -124,7 +140,13 @@ export async function readProcessOutputTool(args, extra = {}) {
|
|
|
124
140
|
}
|
|
125
141
|
const status = describeSession(session);
|
|
126
142
|
const header = `pid ${pid} ${status.status} · lines ${range}`;
|
|
127
|
-
|
|
143
|
+
const output = slice.join('\n');
|
|
144
|
+
return structured({
|
|
145
|
+
...status,
|
|
146
|
+
range,
|
|
147
|
+
output,
|
|
148
|
+
explicitOffset:hasOffset,
|
|
149
|
+
}, `${header}\n${output}`);
|
|
128
150
|
}
|
|
129
151
|
|
|
130
152
|
function compileWaiter(pattern) {
|
|
@@ -164,7 +186,15 @@ export async function waitForProcessOutputTool(args, extra = {}) {
|
|
|
164
186
|
const headline = matched
|
|
165
187
|
? `pid ${pid} ${status.status} · pattern matched${bufferedMatch ? ' (already buffered)' : ''}`
|
|
166
188
|
: `pid ${pid} ${status.status} · pattern not matched within ${timeoutMs}ms`;
|
|
167
|
-
|
|
189
|
+
const output = slice.join('\n');
|
|
190
|
+
return structured({
|
|
191
|
+
...status,
|
|
192
|
+
pattern,
|
|
193
|
+
matched,
|
|
194
|
+
bufferedMatch,
|
|
195
|
+
timeoutMs,
|
|
196
|
+
output,
|
|
197
|
+
}, [headline, output].filter(Boolean).join('\n'));
|
|
168
198
|
}
|
|
169
199
|
|
|
170
200
|
export async function interactWithProcessTool(args, extra = {}) {
|
|
@@ -195,34 +225,49 @@ export async function interactWithProcessTool(args, extra = {}) {
|
|
|
195
225
|
}
|
|
196
226
|
const slice = readNewOutput(session);
|
|
197
227
|
const status = describeSession(session);
|
|
198
|
-
|
|
228
|
+
const output = slice.join('\n');
|
|
229
|
+
return structured({
|
|
230
|
+
...status,
|
|
231
|
+
output,
|
|
232
|
+
}, [`pid ${pid} ${status.status}`, output].filter(Boolean).join('\n'));
|
|
199
233
|
}
|
|
200
234
|
|
|
201
235
|
export async function forceTerminateTool(args) {
|
|
202
236
|
const pid = requireInteger(args.pid, 'pid');
|
|
203
237
|
const session = getProcessSession(pid);
|
|
204
238
|
if (!session) fail(`No ReMCP session with pid ${pid}`);
|
|
205
|
-
if (session.exited)
|
|
239
|
+
if (session.exited) {
|
|
240
|
+
const status = describeSession(session);
|
|
241
|
+
return structured({ ...status, terminated:false, alreadyExited:true, escalated:false }, `Process ${pid} already exited.`);
|
|
242
|
+
}
|
|
206
243
|
killSessionTree(session, 'SIGTERM');
|
|
207
244
|
const deadline = Date.now() + 2000;
|
|
208
245
|
while (!session.exited && Date.now() < deadline) await waitForProcessActivity(session, 100);
|
|
246
|
+
let escalated = false;
|
|
209
247
|
if (!session.exited) {
|
|
248
|
+
escalated = true;
|
|
210
249
|
killSessionTree(session, 'SIGKILL');
|
|
211
250
|
await waitForProcessActivity(session, 1000);
|
|
212
251
|
}
|
|
213
252
|
const status = describeSession(session);
|
|
214
|
-
return
|
|
253
|
+
return structured(
|
|
254
|
+
{ ...status, terminated:Boolean(session.exited), alreadyExited:false, escalated },
|
|
255
|
+
`Terminated session ${pid}${status.status.startsWith('exited') ? '' : ' (still running)'}. Status: ${status.status}.`,
|
|
256
|
+
);
|
|
215
257
|
}
|
|
216
258
|
|
|
217
259
|
export async function listSessionsTool() {
|
|
218
260
|
const sessions = listProcessSessions();
|
|
219
|
-
|
|
220
|
-
const rows = sessions.map(session => {
|
|
261
|
+
const facts = sessions.map(session => {
|
|
221
262
|
const status = describeSession(session);
|
|
222
263
|
const blocked = session.exited ? '' : session.partial ? 'blocked-possibly' : 'idle-or-running';
|
|
223
|
-
return
|
|
264
|
+
return { ...status, blocked, command:session.command.slice(0, 120) };
|
|
224
265
|
});
|
|
225
|
-
|
|
266
|
+
if (!facts.length) return structured({ sessions:[] }, 'No active terminal sessions.');
|
|
267
|
+
const rows = facts.map(session =>
|
|
268
|
+
`pid ${session.pid} · ${session.status} · ${Math.round(session.runtimeMs / 1000)}s · ${session.blocked} · ${session.command}`
|
|
269
|
+
);
|
|
270
|
+
return structured({ sessions:facts }, rows.join('\n'));
|
|
226
271
|
}
|
|
227
272
|
|
|
228
273
|
export const terminalToolHandlers = {
|
package/src/util.mjs
CHANGED
|
@@ -88,8 +88,16 @@ function resolvedRoots() {
|
|
|
88
88
|
// Canonical, allowlist-checked path for every file tool. The canonical path is what
|
|
89
89
|
// callers must use, so a symlink cannot be swapped between the check and the access.
|
|
90
90
|
export async function resolveSafePath(value, field = 'path') {
|
|
91
|
-
const
|
|
91
|
+
const raw = expandHome(requireString(value, field));
|
|
92
|
+
if (raw.includes('\0')) fail(`${field} contains an invalid character`);
|
|
93
|
+
const absolute = path.resolve(raw);
|
|
92
94
|
if (!runtimeConfig.allowedRoots.length) return absolute;
|
|
95
|
+
|
|
96
|
+
// Canonicalize both sides before enforcing confinement. This matters on macOS where
|
|
97
|
+
// /var is a symlink to /private/var: a safe path returned by an earlier canonicalization
|
|
98
|
+
// must not be rejected merely because its lexical prefix differs from the configured root.
|
|
99
|
+
// Symlink escapes remain blocked because the final canonical target must still be inside
|
|
100
|
+
// one of the canonical allowed roots.
|
|
93
101
|
const canonical = await canonicalizePath(absolute);
|
|
94
102
|
const roots = await resolvedRoots();
|
|
95
103
|
if (!roots.some(root => isInsideRoot(canonical, root))) {
|
|
@@ -145,6 +153,21 @@ export function text(value, isError = false) {
|
|
|
145
153
|
return { content: [{ type: 'text', text: rendered }], structuredContent: { text: mirror }, ...(isError ? { isError: true } : {}) };
|
|
146
154
|
}
|
|
147
155
|
|
|
156
|
+
export function structured(value, fallbackText = null) {
|
|
157
|
+
const serialized = JSON.stringify(value);
|
|
158
|
+
const bytes = Buffer.byteLength(serialized ?? 'null', 'utf8');
|
|
159
|
+
const rendered = truncate(fallbackText == null ? JSON.stringify(value, null, 2) : String(fallbackText));
|
|
160
|
+
let structuredContent;
|
|
161
|
+
if (bytes > STRUCTURED_MIRROR_LIMIT_BYTES) {
|
|
162
|
+
structuredContent = { truncated: true, bytes, preview: rendered.slice(0, 2048) };
|
|
163
|
+
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
164
|
+
structuredContent = value;
|
|
165
|
+
} else {
|
|
166
|
+
structuredContent = { data: value };
|
|
167
|
+
}
|
|
168
|
+
return { content: [{ type: 'text', text: rendered }], structuredContent };
|
|
169
|
+
}
|
|
170
|
+
|
|
148
171
|
export function image(data, mimeType) {
|
|
149
172
|
return { type: 'image', data, mimeType };
|
|
150
173
|
}
|