@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/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
+ }
@@ -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 ownership and permissions of that folder, or add ${execPath} to System Settings → Privacy & Security → Full Disk Access and restart the agent with \`remcp start\`.`;
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 absolute = resolveInputPath(value, field);
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
  }