file-share-mcp 1.0.13 → 1.0.15

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
@@ -112,11 +112,21 @@ By default, all user data is stored in `~/.file-share-mcp/`:
112
112
  ~/.file-share-mcp/
113
113
  ├── configs.json # Saved configuration (shared folder path, etc.)
114
114
  ├── shared/ # Default shared folder
115
+ │ └── .uploads-tmp/ # Staging area for in-flight GUI uploads (hidden, purged at startup)
115
116
  └── logs/
116
117
  └── server.log # Application logs
117
118
  ```
118
119
 
119
- This directory survives package updates and reinstalls.
120
+ This directory survives package updates and reinstalls. Both processes (MCP server
121
+ and GUI) log under `<data dir>/logs`, never under the working directory — the cwd of
122
+ an MCP launched by a gateway is not its own and is not necessarily writable.
123
+
124
+ Uploads are staged in `.uploads-tmp` inside the shared folder rather than in the
125
+ system temp directory, so the final move never crosses a mount boundary. `rename()`
126
+ returns `EXDEV` as soon as it does, even when both sides are on the same filesystem —
127
+ which is the case under systemd `PrivateTmp=yes`, with `/tmp` on tmpfs, or with a
128
+ `TMPDIR` on another volume. A copy-then-rename fallback covers the remaining case
129
+ where `MCP_DATA_DIR` and a custom `shared_folder_path` sit on different volumes.
120
130
 
121
131
  ---
122
132
 
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(os.homedir(), '.file-share-mcp', 'logs') });
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
- const upload = multer({ dest: os.tmpdir() });
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
- fs.renameSync(req.file.path, dest);
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) {
@@ -12,7 +12,9 @@ import { getSharedFiles, validateSafePath, writeSharedFile, copyToSharedFolder,
12
12
  import { VERSION } from './version.js';
13
13
  const __filename = fileURLToPath(import.meta.url);
14
14
  const __dirname = path.dirname(__filename);
15
- setupLogging({ processLabel: 'MASTER' });
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',
@@ -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
- return fs.readdirSync(dirPath).map(name => {
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()) {
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '1.0.13';
1
+ export const VERSION = '1.0.15';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "file-share-mcp",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
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",
@@ -37,7 +37,7 @@
37
37
  "test": "echo \"Error: no test specified\" && exit 1"
38
38
  },
39
39
  "dependencies": {
40
- "@imenam/mcp-gui-interface": "^1.0.10",
40
+ "@imenam/mcp-gui-interface": "^1.0.12",
41
41
  "@modelcontextprotocol/sdk": "^1.26.0",
42
42
  "axios": "^1.13.5",
43
43
  "cors": "^2.8.6",