file-share-mcp 1.0.15 → 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
@@ -11,6 +11,11 @@ The agent can list, read, and notify about files. The user can upload, download,
11
11
  - `list_shared_files` — List all files available in the shared folder
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
+ - `write_file` — Write text content to a new file in the shared folder
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
+ - `create_directory` / `delete_directory` — Manage subdirectories
18
+ - `move_file` — Move or rename a file inside the shared folder
14
19
  - **Web GUI** — Upload, download, preview, and delete files from a browser
15
20
  - **Zero config** — works out of the box with sensible defaults (`~/.file-share-mcp`)
16
21
  - **Proxy-ready** — optional integration with the [file-share-mcp proxy](https://github.com/...)
@@ -104,6 +109,53 @@ All configuration is done via environment variables, passed in the MCP `env` blo
104
109
 
105
110
  ---
106
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
+
107
159
  ## Data & Logs
108
160
 
109
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, 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',
@@ -135,6 +143,32 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
135
143
  required: ['source_path'],
136
144
  },
137
145
  },
146
+ {
147
+ name: 'download_file',
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.',
149
+ inputSchema: {
150
+ type: 'object',
151
+ properties: {
152
+ path: {
153
+ type: 'string',
154
+ description: 'Relative path of the file in the shared folder (e.g. "exports/report.pdf").',
155
+ },
156
+ destination_path: {
157
+ type: 'string',
158
+ description: 'ABSOLUTE path of the destination DIRECTORY on the agent machine. It is created if missing, and the file keeps its own name.',
159
+ },
160
+ destination_overwrite: {
161
+ type: 'boolean',
162
+ description: 'Replace the destination file if it already exists. Defaults to false.',
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
+ },
168
+ },
169
+ required: ['path', 'destination_path'],
170
+ },
171
+ },
138
172
  {
139
173
  name: 'create_directory',
140
174
  description: 'Create a new subdirectory inside the shared folder.',
@@ -258,16 +292,61 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
258
292
  };
259
293
  }
260
294
  case 'copy_file': {
261
- const { source_path, filename, path: relativePath } = z.object({
262
- 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(),
263
299
  filename: z.string().optional(),
264
300
  path: z.string().optional(),
265
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
+ }
266
318
  const copiedName = copyToSharedFolder(source_path, filename, relativePath ?? '');
267
319
  return {
268
320
  content: [{ type: 'text', text: `File "${copiedName}" copied to shared folder.` }],
269
321
  };
270
322
  }
323
+ case 'download_file': {
324
+ const { path: relativePath, destination_path, destination_overwrite, destination_relay } = z.object({
325
+ path: z.string(),
326
+ destination_path: z.string().optional(),
327
+ destination_overwrite: z.boolean().optional(),
328
+ destination_relay: z.boolean().optional(),
329
+ }).parse(args);
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);
346
+ return {
347
+ content: [{ type: 'text', text: `File "${relativePath}" downloaded to "${written}".` }],
348
+ };
349
+ }
271
350
  case 'create_directory': {
272
351
  const { path: dirPath } = z.object({ path: z.string() }).parse(args);
273
352
  createDirectory(dirPath);
@@ -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,6 +123,82 @@ 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
+ };
146
+ /**
147
+ * Read a shared file for delivery through the gateway relay.
148
+ *
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.
152
+ */
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) {
181
+ const sourcePath = validateSafePath(relativePath);
182
+ if (!fs.existsSync(sourcePath)) {
183
+ throw new Error(`File not found in shared folder: ${relativePath}`);
184
+ }
185
+ if (!fs.statSync(sourcePath).isFile()) {
186
+ throw new Error(`Path is not a file: ${relativePath}`);
187
+ }
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}`);
193
+ }
194
+ const destPath = path.join(destinationDir, path.basename(sourcePath));
195
+ if (!overwrite && fs.existsSync(destPath)) {
196
+ throw new Error(`Destination already exists: ${destPath}. Pass destination_overwrite=true to replace it.`);
197
+ }
198
+ fs.mkdirSync(destinationDir, { recursive: true });
199
+ fs.copyFileSync(sourcePath, destPath);
200
+ return destPath;
201
+ }
104
202
  export function createDirectory(relativePath) {
105
203
  const fullPath = validateSafePath(relativePath);
106
204
  fs.mkdirSync(fullPath, { recursive: true });
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '1.0.15';
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.15",
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",