file-share-mcp 1.0.14 → 1.0.16
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 +16 -1
- package/dist/gui.js +39 -5
- package/dist/mcp-server.js +37 -2
- package/dist/utils/file-manager.js +73 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
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 workspace into the shared folder
|
|
16
|
+
- `download_file` — Copy a file from the shared folder to the agent machine (any file type, binary included)
|
|
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/...)
|
|
@@ -112,11 +117,21 @@ By default, all user data is stored in `~/.file-share-mcp/`:
|
|
|
112
117
|
~/.file-share-mcp/
|
|
113
118
|
├── configs.json # Saved configuration (shared folder path, etc.)
|
|
114
119
|
├── shared/ # Default shared folder
|
|
120
|
+
│ └── .uploads-tmp/ # Staging area for in-flight GUI uploads (hidden, purged at startup)
|
|
115
121
|
└── logs/
|
|
116
122
|
└── server.log # Application logs
|
|
117
123
|
```
|
|
118
124
|
|
|
119
|
-
This directory survives package updates and reinstalls.
|
|
125
|
+
This directory survives package updates and reinstalls. Both processes (MCP server
|
|
126
|
+
and GUI) log under `<data dir>/logs`, never under the working directory — the cwd of
|
|
127
|
+
an MCP launched by a gateway is not its own and is not necessarily writable.
|
|
128
|
+
|
|
129
|
+
Uploads are staged in `.uploads-tmp` inside the shared folder rather than in the
|
|
130
|
+
system temp directory, so the final move never crosses a mount boundary. `rename()`
|
|
131
|
+
returns `EXDEV` as soon as it does, even when both sides are on the same filesystem —
|
|
132
|
+
which is the case under systemd `PrivateTmp=yes`, with `/tmp` on tmpfs, or with a
|
|
133
|
+
`TMPDIR` on another volume. A copy-then-rename fallback covers the remaining case
|
|
134
|
+
where `MCP_DATA_DIR` and a custom `shared_folder_path` sit on different volumes.
|
|
120
135
|
|
|
121
136
|
---
|
|
122
137
|
|
package/dist/gui.js
CHANGED
|
@@ -4,18 +4,52 @@ import cors from 'cors';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import fs from 'fs';
|
|
7
|
-
import os from 'os';
|
|
8
7
|
import { setupLogging, createLogger, ProxyClient } from '@imenam/mcp-gui-interface';
|
|
9
8
|
import { getConfig, saveConfig } from './utils/config-manager.js';
|
|
10
|
-
import { getSharedFiles, validateSafePath, deleteFile, createDirectory, moveFile, deleteDirectory } from './utils/file-manager.js';
|
|
9
|
+
import { getSharedFiles, validateSafePath, deleteFile, createDirectory, moveFile, deleteDirectory, movePath, UPLOAD_TMP_DIRNAME } from './utils/file-manager.js';
|
|
11
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
11
|
const __dirname = path.dirname(__filename);
|
|
13
|
-
setupLogging({ processLabel: 'GUI', logDir: path.join(
|
|
12
|
+
setupLogging({ processLabel: 'GUI', logDir: path.join(getConfig().MCP_DATA_DIR, 'logs') });
|
|
14
13
|
const logger = createLogger('GUI');
|
|
15
14
|
const app = express();
|
|
16
15
|
app.use(cors());
|
|
17
16
|
app.use(express.json());
|
|
18
|
-
|
|
17
|
+
// Stage uploads inside the shared folder: the destination always lives there, so
|
|
18
|
+
// the final rename stays within one mount. os.tmpdir() cannot promise that — under
|
|
19
|
+
// systemd PrivateTmp, /tmp is a separate mount and rename() returns EXDEV.
|
|
20
|
+
function getUploadTmpDir() {
|
|
21
|
+
const { shared_folder_path } = getConfig();
|
|
22
|
+
const dir = path.join(path.resolve(shared_folder_path), UPLOAD_TMP_DIRNAME);
|
|
23
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
24
|
+
return dir;
|
|
25
|
+
}
|
|
26
|
+
function purgeUploadTmpDir() {
|
|
27
|
+
try {
|
|
28
|
+
const dir = getUploadTmpDir();
|
|
29
|
+
for (const name of fs.readdirSync(dir)) {
|
|
30
|
+
try {
|
|
31
|
+
fs.unlinkSync(path.join(dir, name));
|
|
32
|
+
}
|
|
33
|
+
catch { }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
logger.warn(`Could not purge upload staging dir: ${err.message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
purgeUploadTmpDir();
|
|
41
|
+
const upload = multer({
|
|
42
|
+
storage: multer.diskStorage({
|
|
43
|
+
destination: (_req, _file, cb) => {
|
|
44
|
+
try {
|
|
45
|
+
cb(null, getUploadTmpDir());
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
cb(err, '');
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
19
53
|
// Endpoints
|
|
20
54
|
app.get('/proxy/health', (req, res) => {
|
|
21
55
|
res.json({ status: 'healthy' });
|
|
@@ -104,7 +138,7 @@ app.post('/api/upload', upload.single('file'), (req, res) => {
|
|
|
104
138
|
const dir = validateSafePath(targetPath);
|
|
105
139
|
fs.mkdirSync(dir, { recursive: true });
|
|
106
140
|
const dest = path.join(dir, path.basename(req.file.originalname));
|
|
107
|
-
|
|
141
|
+
movePath(req.file.path, dest);
|
|
108
142
|
res.json({ message: 'File uploaded successfully', filename: path.basename(req.file.originalname) });
|
|
109
143
|
}
|
|
110
144
|
catch (err) {
|
package/dist/mcp-server.js
CHANGED
|
@@ -8,11 +8,13 @@ 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, copyFromSharedFolder, 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);
|
|
15
|
-
|
|
15
|
+
// Always log under MCP_DATA_DIR: the cwd of an MCP launched by a gateway is not
|
|
16
|
+
// ours and is not necessarily writable (the library default is <cwd>/.mcp-gui/logs).
|
|
17
|
+
setupLogging({ processLabel: 'MASTER', logDir: path.join(getConfig().MCP_DATA_DIR, 'logs') });
|
|
16
18
|
const logger = createLogger('MASTER');
|
|
17
19
|
const server = new Server({
|
|
18
20
|
name: 'file-share-mcp',
|
|
@@ -133,6 +135,28 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
133
135
|
required: ['source_path'],
|
|
134
136
|
},
|
|
135
137
|
},
|
|
138
|
+
{
|
|
139
|
+
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.',
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
properties: {
|
|
144
|
+
path: {
|
|
145
|
+
type: 'string',
|
|
146
|
+
description: 'Relative path of the file in the shared folder (e.g. "exports/report.pdf").',
|
|
147
|
+
},
|
|
148
|
+
destination_path: {
|
|
149
|
+
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.',
|
|
151
|
+
},
|
|
152
|
+
overwrite: {
|
|
153
|
+
type: 'boolean',
|
|
154
|
+
description: 'Replace the destination file if it already exists. Defaults to false.',
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
required: ['path', 'destination_path'],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
136
160
|
{
|
|
137
161
|
name: 'create_directory',
|
|
138
162
|
description: 'Create a new subdirectory inside the shared folder.',
|
|
@@ -266,6 +290,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
266
290
|
content: [{ type: 'text', text: `File "${copiedName}" copied to shared folder.` }],
|
|
267
291
|
};
|
|
268
292
|
}
|
|
293
|
+
case 'download_file': {
|
|
294
|
+
const { path: relativePath, destination_path, overwrite } = z.object({
|
|
295
|
+
path: z.string(),
|
|
296
|
+
destination_path: z.string(),
|
|
297
|
+
overwrite: z.boolean().optional(),
|
|
298
|
+
}).parse(args);
|
|
299
|
+
const written = copyFromSharedFolder(relativePath, destination_path, overwrite ?? false);
|
|
300
|
+
return {
|
|
301
|
+
content: [{ type: 'text', text: `File "${relativePath}" downloaded to "${written}".` }],
|
|
302
|
+
};
|
|
303
|
+
}
|
|
269
304
|
case 'create_directory': {
|
|
270
305
|
const { path: dirPath } = z.object({ path: z.string() }).parse(args);
|
|
271
306
|
createDirectory(dirPath);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { getConfig } from './config-manager.js';
|
|
4
|
+
/** Staging directory for uploads, kept inside the shared folder so the final
|
|
5
|
+
* rename never crosses a mount boundary. Hidden from listings. */
|
|
6
|
+
export const UPLOAD_TMP_DIRNAME = '.uploads-tmp';
|
|
4
7
|
export function validateSafePath(relativePath) {
|
|
5
8
|
const { shared_folder_path } = getConfig();
|
|
6
9
|
const resolvedBase = path.resolve(shared_folder_path);
|
|
@@ -10,13 +13,52 @@ export function validateSafePath(relativePath) {
|
|
|
10
13
|
}
|
|
11
14
|
return fullPath;
|
|
12
15
|
}
|
|
16
|
+
const PARTIAL_UPLOAD_RE = /^\..*\.part-\d+-\d+$/;
|
|
17
|
+
function isPartialUpload(name) {
|
|
18
|
+
return PARTIAL_UPLOAD_RE.test(name);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Move `srcPath` onto `destPath`, atomically whenever the kernel allows it.
|
|
22
|
+
*
|
|
23
|
+
* `rename()` fails with EXDEV as soon as it crosses a mount boundary, even when
|
|
24
|
+
* both sides live on the same filesystem — which is what happens under systemd
|
|
25
|
+
* `PrivateTmp=yes`, with `/tmp` on tmpfs, or with a TMPDIR on another volume.
|
|
26
|
+
* The fallback copies next to the destination first, then renames within that
|
|
27
|
+
* same directory, so readers never observe a half-written file either way.
|
|
28
|
+
*/
|
|
29
|
+
export function movePath(srcPath, destPath) {
|
|
30
|
+
try {
|
|
31
|
+
fs.renameSync(srcPath, destPath);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
if (err.code !== 'EXDEV')
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
const staged = path.join(path.dirname(destPath), `.${path.basename(destPath)}.part-${process.pid}-${Date.now()}`);
|
|
39
|
+
try {
|
|
40
|
+
fs.copyFileSync(srcPath, staged);
|
|
41
|
+
fs.renameSync(staged, destPath);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
try {
|
|
45
|
+
fs.unlinkSync(staged);
|
|
46
|
+
}
|
|
47
|
+
catch { }
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
fs.unlinkSync(srcPath);
|
|
51
|
+
}
|
|
13
52
|
export function getSharedFiles(relativePath = '') {
|
|
14
53
|
const dirPath = validateSafePath(relativePath);
|
|
15
54
|
if (!fs.existsSync(dirPath)) {
|
|
16
55
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
17
56
|
return [];
|
|
18
57
|
}
|
|
19
|
-
|
|
58
|
+
const hidden = relativePath === '' ? new Set([UPLOAD_TMP_DIRNAME]) : new Set();
|
|
59
|
+
return fs.readdirSync(dirPath).filter(name => {
|
|
60
|
+
return !hidden.has(name) && !isPartialUpload(name);
|
|
61
|
+
}).map(name => {
|
|
20
62
|
const fullPath = path.join(dirPath, name);
|
|
21
63
|
const stats = fs.statSync(fullPath);
|
|
22
64
|
if (stats.isDirectory()) {
|
|
@@ -59,6 +101,36 @@ export function copyToSharedFolder(sourcePath, filename, relativePath = '') {
|
|
|
59
101
|
fs.copyFileSync(sourcePath, path.join(dir, targetFilename));
|
|
60
102
|
return relativePath ? `${relativePath}/${targetFilename}` : targetFilename;
|
|
61
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Copy a file out of the shared folder onto the agent machine.
|
|
106
|
+
*
|
|
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.
|
|
109
|
+
*/
|
|
110
|
+
export function copyFromSharedFolder(relativePath, destination, overwrite = false) {
|
|
111
|
+
const sourcePath = validateSafePath(relativePath);
|
|
112
|
+
if (!fs.existsSync(sourcePath)) {
|
|
113
|
+
throw new Error(`File not found in shared folder: ${relativePath}`);
|
|
114
|
+
}
|
|
115
|
+
if (!fs.statSync(sourcePath).isFile()) {
|
|
116
|
+
throw new Error(`Path is not a file: ${relativePath}`);
|
|
117
|
+
}
|
|
118
|
+
if (!path.isAbsolute(destination)) {
|
|
119
|
+
throw new Error(`Destination must be an absolute path: ${destination}`);
|
|
120
|
+
}
|
|
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;
|
|
127
|
+
if (!overwrite && fs.existsSync(destPath)) {
|
|
128
|
+
throw new Error(`Destination already exists: ${destPath}. Pass overwrite=true to replace it.`);
|
|
129
|
+
}
|
|
130
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
131
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
132
|
+
return destPath;
|
|
133
|
+
}
|
|
62
134
|
export function createDirectory(relativePath) {
|
|
63
135
|
const fullPath = validateSafePath(relativePath);
|
|
64
136
|
fs.mkdirSync(fullPath, { recursive: true });
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '1.0.
|
|
1
|
+
export const VERSION = '1.0.16';
|