file-share-mcp 1.0.16 → 1.0.17

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 CHANGED
@@ -12,8 +12,8 @@ The agent can list, read, and notify about files. The user can upload, download,
12
12
  - `share_file` — Notify the user that a file is ready to download
13
13
  - `preview_file` — Read the content of a `.txt` or `.md` file (max 1 MB)
14
14
  - `write_file` — Write text content to a new file in the shared folder
15
- - `copy_file` — Copy a file from the agent workspace into the shared folder
16
- - `download_file` — Copy a file from the shared folder to the agent machine (any file type, binary included)
15
+ - `copy_file` — Copy a file from the agent machine into the shared folder (see [Moving files across machines](#moving-files-across-machines))
16
+ - `download_file` — Copy a file from the shared folder onto the agent machine (any file type, binary included — see [Downloading to the agent machine](#downloading-to-the-agent-machine))
17
17
  - `create_directory` / `delete_directory` — Manage subdirectories
18
18
  - `move_file` — Move or rename a file inside the shared folder
19
19
  - **Web GUI** — Upload, download, preview, and delete files from a browser
@@ -109,6 +109,53 @@ All configuration is done via environment variables, passed in the MCP `env` blo
109
109
 
110
110
  ---
111
111
 
112
+ ## Moving files across machines
113
+
114
+ Two tools move a file between the agent and the shared folder, in opposite directions:
115
+
116
+ | Tool | Direction | Names the file with |
117
+ |---|---|---|
118
+ | `copy_file` | agent machine → shared folder | `source_path` (absolute file path) |
119
+ | `download_file` | shared folder → agent machine | `destination_path` (absolute directory) |
120
+
121
+ Both are straightforward **over stdio**, the usual case where the MCP server is a child
122
+ process of the agent: one filesystem, and the paths mean the same thing on both sides.
123
+ Nothing to configure.
124
+
125
+ **Through [`@imenam/mcp-http-gateway`](https://www.npmjs.com/package/@imenam/mcp-http-gateway)**
126
+ the server runs on the gateway machine, and neither path means anything there. MCP has no
127
+ file-transfer primitive — only the bytes a message carries actually travel — so the relay,
128
+ which runs next to the agent in `--mcp` mode, fills the gap in both directions. Configure it
129
+ with `GATEWAY_INLINE_ROOTS` (reading, for `copy_file`), `GATEWAY_INLINE_WRITE_ROOTS`
130
+ (writing, for `download_file`) and `GATEWAY_INLINE_EXTENSIONS: "*"` — its default only covers
131
+ spreadsheets, which a general file share has no reason to be limited to.
132
+
133
+ Neither direction has a fallback. With the relay in the chain but the matching root
134
+ unconfigured, the call fails and names the variable to set; nothing is read, written, or
135
+ dumped into the conversation.
136
+
137
+ ### Uploading: `copy_file`
138
+
139
+ `source_path` is the absolute path of the file on the agent machine. Through the relay, the
140
+ hidden `source_content` / `source_name` parameters carry the file itself; the agent only ever
141
+ names a path.
142
+
143
+ ### Downloading: `download_file`
144
+
145
+ `destination_path` is the **absolute directory** where the file should land on the agent
146
+ machine. The file keeps its own name; `destination_overwrite` (default `false`) governs
147
+ replacing an existing file.
148
+ **absolute directory** where it should land on the agent machine. The file keeps its own
149
+ name; `destination_overwrite` (default `false`) governs replacing an existing file.
150
+
151
+ Through the relay, the hidden `destination_relay` flag tells the server the destination is
152
+ on another machine: it then returns the file's bytes in its response, and the relay writes
153
+ them and strips them from the result. The bytes never reach the agent's context — an 8 MiB
154
+ file base64-encoded into a conversation would be costly, and an agent re-typing it by hand
155
+ would corrupt it.
156
+
157
+ ---
158
+
112
159
  ## Data & Logs
113
160
 
114
161
  By default, all user data is stored in `~/.file-share-mcp/`:
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'url';
8
8
  import fs from 'fs';
9
9
  import { GuiLauncher, setupLogging, createLogger, reconnectGuiToolDefinition } from '@imenam/mcp-gui-interface';
10
10
  import { getConfig } from './utils/config-manager.js';
11
- import { getSharedFiles, validateSafePath, writeSharedFile, copyToSharedFolder, copyFromSharedFolder, createDirectory, deleteDirectory, moveFile, } from './utils/file-manager.js';
11
+ import { getSharedFiles, validateSafePath, writeSharedFile, copyToSharedFolder, writeSharedFileFromBase64, copyFromSharedFolder, readForRelay, createDirectory, deleteDirectory, moveFile, } from './utils/file-manager.js';
12
12
  import { VERSION } from './version.js';
13
13
  const __filename = fileURLToPath(import.meta.url);
14
14
  const __dirname = path.dirname(__filename);
@@ -121,7 +121,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
121
121
  properties: {
122
122
  source_path: {
123
123
  type: 'string',
124
- description: 'Absolute path to the source file in the agent workspace.',
124
+ description: 'Absolute path to the source file on the agent machine.',
125
+ },
126
+ source_content: {
127
+ type: 'string',
128
+ description: 'Filled in by the mcp-http-gateway relay, never by the agent: the base64 content of the file read on the agent machine.',
129
+ },
130
+ source_name: {
131
+ type: 'string',
132
+ description: 'Filled in by the mcp-http-gateway relay, never by the agent: the original name of the file read on the agent machine.',
125
133
  },
126
134
  filename: {
127
135
  type: 'string',
@@ -137,7 +145,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
137
145
  },
138
146
  {
139
147
  name: 'download_file',
140
- description: 'Download a file from the shared folder to the agent workspace (the reverse of copy_file). Works for any file type, including binary ones.',
148
+ description: 'Download a file from the shared folder onto the agent machine. Works for any file type, including binary ones. The file never passes through the conversation.',
141
149
  inputSchema: {
142
150
  type: 'object',
143
151
  properties: {
@@ -147,12 +155,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
147
155
  },
148
156
  destination_path: {
149
157
  type: 'string',
150
- description: 'ABSOLUTE destination path on the agent machine: either an existing directory (the file keeps its name) or the full target file path. Missing parent directories are created.',
158
+ description: 'ABSOLUTE path of the destination DIRECTORY on the agent machine. It is created if missing, and the file keeps its own name.',
151
159
  },
152
- overwrite: {
160
+ destination_overwrite: {
153
161
  type: 'boolean',
154
162
  description: 'Replace the destination file if it already exists. Defaults to false.',
155
163
  },
164
+ destination_relay: {
165
+ type: 'boolean',
166
+ description: 'Filled in by the mcp-http-gateway relay, never by the agent. Signals that the destination lives on another machine and that the file content must travel in the response.',
167
+ },
156
168
  },
157
169
  required: ['path', 'destination_path'],
158
170
  },
@@ -280,23 +292,57 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
280
292
  };
281
293
  }
282
294
  case 'copy_file': {
283
- const { source_path, filename, path: relativePath } = z.object({
284
- source_path: z.string(),
295
+ const { source_path, source_content, source_name, filename, path: relativePath } = z.object({
296
+ source_path: z.string().optional(),
297
+ source_content: z.string().optional(),
298
+ source_name: z.string().optional(),
285
299
  filename: z.string().optional(),
286
300
  path: z.string().optional(),
287
301
  }).parse(args);
302
+ // Le relais a lu le fichier sur la machine de l'agent : son chemin ne désigne
303
+ // rien ici, seuls les octets sont exploitables.
304
+ if (source_content !== undefined) {
305
+ const target = filename ?? source_name;
306
+ if (!target) {
307
+ throw new Error('source_name or filename is required when the content is provided.');
308
+ }
309
+ const copiedName = writeSharedFileFromBase64(source_content, target, relativePath ?? '');
310
+ return {
311
+ content: [{ type: 'text', text: `File "${copiedName}" copied to shared folder.` }],
312
+ };
313
+ }
314
+ // Pas de relais : le MCP tourne à côté de l'agent, son disque est le bon.
315
+ if (!source_path) {
316
+ throw new Error('source_path is required.');
317
+ }
288
318
  const copiedName = copyToSharedFolder(source_path, filename, relativePath ?? '');
289
319
  return {
290
320
  content: [{ type: 'text', text: `File "${copiedName}" copied to shared folder.` }],
291
321
  };
292
322
  }
293
323
  case 'download_file': {
294
- const { path: relativePath, destination_path, overwrite } = z.object({
324
+ const { path: relativePath, destination_path, destination_overwrite, destination_relay } = z.object({
295
325
  path: z.string(),
296
- destination_path: z.string(),
297
- overwrite: z.boolean().optional(),
326
+ destination_path: z.string().optional(),
327
+ destination_overwrite: z.boolean().optional(),
328
+ destination_relay: z.boolean().optional(),
298
329
  }).parse(args);
299
- const written = copyFromSharedFolder(relativePath, destination_path, overwrite ?? false);
330
+ // Le relais tourne sur la machine de l'agent et a retenu la destination : il
331
+ // attend les octets, qu'il écrira lui-même. Écrire ici viserait le mauvais disque.
332
+ if (destination_relay) {
333
+ const { blob, mimeType, name } = readForRelay(relativePath);
334
+ return {
335
+ content: [{
336
+ type: 'resource',
337
+ resource: { uri: `file:///${name}`, name, mimeType, blob },
338
+ }],
339
+ };
340
+ }
341
+ // Pas de relais : le MCP tourne à côté de l'agent, son disque est le bon.
342
+ if (!destination_path) {
343
+ throw new Error('destination_path is required.');
344
+ }
345
+ const written = copyFromSharedFolder(relativePath, destination_path, destination_overwrite ?? false);
300
346
  return {
301
347
  content: [{ type: 'text', text: `File "${relativePath}" downloaded to "${written}".` }],
302
348
  };
@@ -90,6 +90,28 @@ export function writeSharedFile(filename, content, relativePath = '') {
90
90
  fs.writeFileSync(dest, content, 'utf-8');
91
91
  return dest;
92
92
  }
93
+ /**
94
+ * Write into the shared folder a file whose content travelled base64-encoded.
95
+ *
96
+ * Used when the gateway relay has read the file on the agent machine and inlined it:
97
+ * the server never sees the agent's disk, so the bytes are all it gets.
98
+ */
99
+ export function writeSharedFileFromBase64(base64, filename, relativePath = '') {
100
+ // Le relais peut préfixer le contenu façon data-URI ; on l'accepte des deux formes.
101
+ const payload = base64.replace(/^data:[^;,]*;base64,/, '');
102
+ const data = Buffer.from(payload, 'base64');
103
+ if (data.length > RELAY_MAX_BYTES) {
104
+ throw new Error(`File too large to receive: ${data.length} bytes, the limit is ${RELAY_MAX_BYTES}.`);
105
+ }
106
+ const targetFilename = path.basename(filename);
107
+ if (!targetFilename || targetFilename === '.' || targetFilename === '..') {
108
+ throw new Error(`Invalid target filename: ${filename}`);
109
+ }
110
+ const dir = validateSafePath(relativePath);
111
+ fs.mkdirSync(dir, { recursive: true });
112
+ fs.writeFileSync(path.join(dir, targetFilename), data);
113
+ return relativePath ? `${relativePath}/${targetFilename}` : targetFilename;
114
+ }
93
115
  export function copyToSharedFolder(sourcePath, filename, relativePath = '') {
94
116
  const targetFilename = filename ? path.basename(filename) : path.basename(sourcePath);
95
117
  const stats = fs.statSync(sourcePath);
@@ -101,13 +123,61 @@ export function copyToSharedFolder(sourcePath, filename, relativePath = '') {
101
123
  fs.copyFileSync(sourcePath, path.join(dir, targetFilename));
102
124
  return relativePath ? `${relativePath}/${targetFilename}` : targetFilename;
103
125
  }
126
+ /** Hard ceiling on what may be encoded into a tool response. The relay applies its own,
127
+ * configurable limit; this one only exists so a huge file fails cleanly here instead of
128
+ * exhausting memory while being base64-encoded. */
129
+ const RELAY_MAX_BYTES = 32 * 1024 * 1024;
130
+ const MIME_BY_EXTENSION = {
131
+ '.csv': 'text/csv',
132
+ '.htm': 'text/html',
133
+ '.html': 'text/html',
134
+ '.jpeg': 'image/jpeg',
135
+ '.jpg': 'image/jpeg',
136
+ '.json': 'application/json',
137
+ '.md': 'text/markdown',
138
+ '.pdf': 'application/pdf',
139
+ '.png': 'image/png',
140
+ '.svg': 'image/svg+xml',
141
+ '.txt': 'text/plain',
142
+ '.xls': 'application/vnd.ms-excel',
143
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
144
+ '.zip': 'application/zip',
145
+ };
104
146
  /**
105
- * Copy a file out of the shared folder onto the agent machine.
147
+ * Read a shared file for delivery through the gateway relay.
106
148
  *
107
- * `destination` is either a directory (existing, or written with a trailing
108
- * separator) — the file then keeps its name — or the full target file path.
149
+ * The bytes travel base64-encoded inside the tool response, and the relay — which runs
150
+ * on the agent machine — writes them there and strips them from the result. They are
151
+ * never meant to reach the agent's context.
109
152
  */
110
- export function copyFromSharedFolder(relativePath, destination, overwrite = false) {
153
+ export function readForRelay(relativePath) {
154
+ const sourcePath = validateSafePath(relativePath);
155
+ if (!fs.existsSync(sourcePath)) {
156
+ throw new Error(`File not found in shared folder: ${relativePath}`);
157
+ }
158
+ const stats = fs.statSync(sourcePath);
159
+ if (!stats.isFile()) {
160
+ throw new Error(`Path is not a file: ${relativePath}`);
161
+ }
162
+ if (stats.size > RELAY_MAX_BYTES) {
163
+ throw new Error(`File too large to transfer: ${relativePath} weighs ${stats.size} bytes, the limit is ${RELAY_MAX_BYTES}.`);
164
+ }
165
+ const name = path.basename(sourcePath);
166
+ return {
167
+ blob: fs.readFileSync(sourcePath).toString('base64'),
168
+ mimeType: MIME_BY_EXTENSION[path.extname(name).toLowerCase()] ?? 'application/octet-stream',
169
+ name,
170
+ };
171
+ }
172
+ /**
173
+ * Copy a file out of the shared folder into a directory on the agent machine.
174
+ *
175
+ * `destinationDir` is always a directory, created if missing, and the file keeps its
176
+ * name. Letting it mean either a directory or a file path would be ambiguous for a
177
+ * path that does not exist yet — and the relay, which writes the remote half of this
178
+ * same tool, has no way to guess either.
179
+ */
180
+ export function copyFromSharedFolder(relativePath, destinationDir, overwrite = false) {
111
181
  const sourcePath = validateSafePath(relativePath);
112
182
  if (!fs.existsSync(sourcePath)) {
113
183
  throw new Error(`File not found in shared folder: ${relativePath}`);
@@ -115,19 +185,17 @@ export function copyFromSharedFolder(relativePath, destination, overwrite = fals
115
185
  if (!fs.statSync(sourcePath).isFile()) {
116
186
  throw new Error(`Path is not a file: ${relativePath}`);
117
187
  }
118
- if (!path.isAbsolute(destination)) {
119
- throw new Error(`Destination must be an absolute path: ${destination}`);
188
+ if (!path.isAbsolute(destinationDir)) {
189
+ throw new Error(`destination_path must be an absolute directory path: ${destinationDir}`);
190
+ }
191
+ if (fs.existsSync(destinationDir) && !fs.statSync(destinationDir).isDirectory()) {
192
+ throw new Error(`destination_path must be a directory, but a file already exists there: ${destinationDir}`);
120
193
  }
121
- const looksLikeDir = destination.endsWith('/') ||
122
- destination.endsWith(path.sep) ||
123
- (fs.existsSync(destination) && fs.statSync(destination).isDirectory());
124
- const destPath = looksLikeDir
125
- ? path.join(destination, path.basename(sourcePath))
126
- : destination;
194
+ const destPath = path.join(destinationDir, path.basename(sourcePath));
127
195
  if (!overwrite && fs.existsSync(destPath)) {
128
- throw new Error(`Destination already exists: ${destPath}. Pass overwrite=true to replace it.`);
196
+ throw new Error(`Destination already exists: ${destPath}. Pass destination_overwrite=true to replace it.`);
129
197
  }
130
- fs.mkdirSync(path.dirname(destPath), { recursive: true });
198
+ fs.mkdirSync(destinationDir, { recursive: true });
131
199
  fs.copyFileSync(sourcePath, destPath);
132
200
  return destPath;
133
201
  }
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '1.0.16';
1
+ export const VERSION = '1.0.17';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "file-share-mcp",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "description": "MCP server for bidirectional file sharing between AI agents and users, with a built-in web GUI",
5
5
  "author": "",
6
6
  "license": "ISC",