@remcp/runtime 0.2.46 → 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.
- package/CHANGELOG.md +19 -0
- package/README.md +42 -25
- package/package.json +6 -2
- package/src/catalog.mjs +1 -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/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/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
|
}
|