file-share-mcp 1.0.16 → 1.0.18
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 +49 -2
- package/dist/mcp-server.js +57 -11
- package/dist/utils/file-manager.js +82 -14
- package/dist/version.js +1 -1
- package/package.json +1 -1
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
|
|
16
|
-
- `download_file` — Copy a file from 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
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`) and `GATEWAY_INLINE_WRITE_ROOTS`
|
|
130
|
+
(writing, for `download_file`). Those two directory lists are the whole configuration: every
|
|
131
|
+
file type is allowed by default.
|
|
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/`:
|
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
324
|
+
const { path: relativePath, destination_path, destination_overwrite, destination_relay } = z.object({
|
|
295
325
|
path: z.string(),
|
|
296
|
-
destination_path: z.string(),
|
|
297
|
-
|
|
326
|
+
destination_path: z.string().optional(),
|
|
327
|
+
destination_overwrite: z.boolean().optional(),
|
|
328
|
+
destination_relay: z.boolean().optional(),
|
|
298
329
|
}).parse(args);
|
|
299
|
-
|
|
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
|
-
*
|
|
147
|
+
* Read a shared file for delivery through the gateway relay.
|
|
106
148
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
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
|
|
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(
|
|
119
|
-
throw new Error(`
|
|
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
|
|
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
|
|
196
|
+
throw new Error(`Destination already exists: ${destPath}. Pass destination_overwrite=true to replace it.`);
|
|
129
197
|
}
|
|
130
|
-
fs.mkdirSync(
|
|
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.
|
|
1
|
+
export const VERSION = '1.0.18';
|