file-share-mcp 1.0.2 → 1.0.5

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/dist/gui.js CHANGED
@@ -4,50 +4,42 @@ 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';
7
8
  import axios from 'axios';
8
9
  import { setupLogging } from './utils/logger.js';
9
10
  import { getConfig, saveConfig } from './utils/config-manager.js';
10
- import { getSharedFiles, validateSafePath, deleteFile } from './utils/file-manager.js';
11
+ import { getSharedFiles, validateSafePath, deleteFile, createDirectory, moveFile, deleteDirectory } from './utils/file-manager.js';
11
12
  const __filename = fileURLToPath(import.meta.url);
12
13
  const __dirname = path.dirname(__filename);
13
14
  setupLogging('GUI');
14
15
  const app = express();
15
16
  app.use(cors());
16
17
  app.use(express.json());
17
- // Protect stdout - setupLogging already redirects console.log/error
18
- // but we want to be sure no other library writes to stdout.
19
- // Multer configuration for file uploads
20
- const storage = multer.diskStorage({
21
- destination: (req, file, cb) => {
22
- const config = getConfig();
23
- if (!fs.existsSync(config.shared_folder_path)) {
24
- fs.mkdirSync(config.shared_folder_path, { recursive: true });
25
- }
26
- cb(null, config.shared_folder_path);
27
- },
28
- filename: (req, file, cb) => {
29
- cb(null, file.originalname);
30
- }
31
- });
32
- const upload = multer({ storage });
18
+ const upload = multer({ dest: os.tmpdir() });
33
19
  // Endpoints
34
20
  app.get('/proxy/health', (req, res) => {
35
21
  res.json({ status: 'healthy' });
36
22
  });
37
23
  app.get('/api/files', (req, res) => {
38
24
  try {
39
- const files = getSharedFiles();
40
- res.json(files);
25
+ const relativePath = req.query.path || '';
26
+ const entries = getSharedFiles(relativePath);
27
+ res.json(entries);
41
28
  }
42
29
  catch (error) {
43
30
  res.status(500).json({ error: error.message });
44
31
  }
45
32
  });
46
- app.get('/api/download/:filename', (req, res) => {
33
+ app.get('/api/download', async (req, res) => {
47
34
  try {
48
- const safePath = validateSafePath(req.params.filename);
49
- if (fs.existsSync(safePath)) {
50
- res.download(safePath);
35
+ const relativePath = req.query.path || '';
36
+ const safePath = validateSafePath(relativePath);
37
+ if (fs.existsSync(safePath) && fs.statSync(safePath).isFile()) {
38
+ res.download(safePath, path.basename(safePath), (err) => {
39
+ if (err && !res.headersSent) {
40
+ res.status(500).send('Download failed');
41
+ }
42
+ });
51
43
  }
52
44
  else {
53
45
  res.status(404).send('File not found');
@@ -57,9 +49,10 @@ app.get('/api/download/:filename', (req, res) => {
57
49
  res.status(400).send(error.message);
58
50
  }
59
51
  });
60
- app.delete('/api/files/:filename', (req, res) => {
52
+ app.delete('/api/files', (req, res) => {
61
53
  try {
62
- deleteFile(req.params.filename);
54
+ const relativePath = req.query.path || '';
55
+ deleteFile(relativePath);
63
56
  res.json({ message: 'File deleted successfully' });
64
57
  }
65
58
  catch (error) {
@@ -68,13 +61,14 @@ app.delete('/api/files/:filename', (req, res) => {
68
61
  });
69
62
  const PREVIEWABLE_EXTENSIONS = ['.txt', '.md'];
70
63
  const PREVIEW_MAX_SIZE = 1024 * 1024; // 1 MB
71
- app.get('/api/preview/:filename', (req, res) => {
64
+ app.get('/api/preview', (req, res) => {
72
65
  try {
73
- const ext = path.extname(req.params.filename).toLowerCase();
66
+ const relativePath = req.query.path || '';
67
+ const ext = path.extname(relativePath).toLowerCase();
74
68
  if (!PREVIEWABLE_EXTENSIONS.includes(ext)) {
75
69
  return res.status(400).json({ error: 'Preview not supported for this file type' });
76
70
  }
77
- const safePath = validateSafePath(req.params.filename);
71
+ const safePath = validateSafePath(relativePath);
78
72
  if (!fs.existsSync(safePath)) {
79
73
  return res.status(404).json({ error: 'File not found' });
80
74
  }
@@ -90,11 +84,56 @@ app.get('/api/preview/:filename', (req, res) => {
90
84
  }
91
85
  });
92
86
  app.post('/api/upload', upload.single('file'), (req, res) => {
93
- if (req.file) {
94
- res.json({ message: 'File uploaded successfully', filename: req.file.filename });
87
+ if (!req.file)
88
+ return res.status(400).json({ error: 'No file uploaded' });
89
+ const targetPath = req.body.targetPath || '';
90
+ try {
91
+ const dir = validateSafePath(targetPath);
92
+ fs.mkdirSync(dir, { recursive: true });
93
+ const dest = path.join(dir, path.basename(req.file.originalname));
94
+ fs.renameSync(req.file.path, dest);
95
+ res.json({ message: 'File uploaded successfully', filename: path.basename(req.file.originalname) });
95
96
  }
96
- else {
97
- res.status(400).json({ error: 'No file uploaded' });
97
+ catch (err) {
98
+ try {
99
+ fs.unlinkSync(req.file.path);
100
+ }
101
+ catch { }
102
+ res.status(400).json({ error: err.message });
103
+ }
104
+ });
105
+ app.delete('/api/folder', (req, res) => {
106
+ try {
107
+ const relativePath = req.query.path || '';
108
+ deleteDirectory(relativePath);
109
+ res.json({ message: 'Directory deleted successfully' });
110
+ }
111
+ catch (error) {
112
+ res.status(400).json({ error: error.message });
113
+ }
114
+ });
115
+ app.post('/api/mkdir', (req, res) => {
116
+ try {
117
+ const { path: dirPath } = req.body;
118
+ if (!dirPath)
119
+ return res.status(400).json({ error: 'path is required' });
120
+ createDirectory(dirPath);
121
+ res.json({ message: 'Directory created successfully' });
122
+ }
123
+ catch (error) {
124
+ res.status(400).json({ error: error.message });
125
+ }
126
+ });
127
+ app.post('/api/move', (req, res) => {
128
+ try {
129
+ const { from, to } = req.body;
130
+ if (!from || !to)
131
+ return res.status(400).json({ error: 'from and to are required' });
132
+ moveFile(from, to);
133
+ res.json({ message: 'File moved successfully' });
134
+ }
135
+ catch (error) {
136
+ res.status(400).json({ error: error.message });
98
137
  }
99
138
  });
100
139
  app.get('/api/config', (req, res) => {
@@ -155,7 +194,6 @@ async function registerWithProxy() {
155
194
  if (!config.PROXY_URL || !config.PROXY_APP_PATH)
156
195
  return null;
157
196
  try {
158
- // Check if proxy is up
159
197
  await axios.get(`${config.PROXY_URL}/proxy/health`, { timeout: 2000 });
160
198
  const response = await axios.post(`${config.PROXY_URL}/proxy/register`, {
161
199
  path: config.PROXY_APP_PATH,
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'url';
9
9
  import fs from 'fs';
10
10
  import { setupLogging } from './utils/logger.js';
11
11
  import { getConfig } from './utils/config-manager.js';
12
- import { getSharedFiles, validateSafePath, writeSharedFile, copyToSharedFolder } from './utils/file-manager.js';
12
+ import { getSharedFiles, validateSafePath, writeSharedFile, copyToSharedFolder, createDirectory, deleteDirectory, moveFile, } from './utils/file-manager.js';
13
13
  import { VERSION } from './version.js';
14
14
  const __filename = fileURLToPath(import.meta.url);
15
15
  const __dirname = path.dirname(__filename);
@@ -99,10 +99,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
99
99
  },
100
100
  {
101
101
  name: 'list_shared_files',
102
- description: 'List all files currently available in the shared folder.',
102
+ description: 'List files and directories in the shared folder. Use the optional path parameter to navigate into a subdirectory.',
103
103
  inputSchema: {
104
104
  type: 'object',
105
- properties: {},
105
+ properties: {
106
+ path: {
107
+ type: 'string',
108
+ description: 'Relative path of the subdirectory to list (e.g. "docs/reports"). Defaults to the root shared folder.',
109
+ },
110
+ },
106
111
  },
107
112
  },
108
113
  {
@@ -125,12 +130,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
125
130
  inputSchema: {
126
131
  type: 'object',
127
132
  properties: {
128
- filename: {
133
+ path: {
129
134
  type: 'string',
130
- description: 'The name of the file to preview (filename only, not the full path).',
135
+ description: 'Relative path to the file in the shared folder (e.g. "docs/notes.md").',
131
136
  },
132
137
  },
133
- required: ['filename'],
138
+ required: ['path'],
134
139
  },
135
140
  },
136
141
  {
@@ -147,6 +152,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
147
152
  type: 'string',
148
153
  description: 'Text content to write into the file.',
149
154
  },
155
+ path: {
156
+ type: 'string',
157
+ description: 'Target subdirectory relative to the shared folder (e.g. "docs/reports"). The directory is created if it does not exist.',
158
+ },
150
159
  },
151
160
  required: ['filename', 'content'],
152
161
  },
@@ -165,10 +174,60 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
165
174
  type: 'string',
166
175
  description: 'Optional target filename in the shared folder. Defaults to the source file basename.',
167
176
  },
177
+ path: {
178
+ type: 'string',
179
+ description: 'Target subdirectory relative to the shared folder (e.g. "exports"). The directory is created if it does not exist.',
180
+ },
168
181
  },
169
182
  required: ['source_path'],
170
183
  },
171
184
  },
185
+ {
186
+ name: 'create_directory',
187
+ description: 'Create a new subdirectory inside the shared folder.',
188
+ inputSchema: {
189
+ type: 'object',
190
+ properties: {
191
+ path: {
192
+ type: 'string',
193
+ description: 'Relative path of the directory to create (e.g. "docs/reports"). Parent directories are created automatically.',
194
+ },
195
+ },
196
+ required: ['path'],
197
+ },
198
+ },
199
+ {
200
+ name: 'delete_directory',
201
+ description: 'Delete an empty subdirectory from the shared folder. Fails if the directory contains files or subdirectories.',
202
+ inputSchema: {
203
+ type: 'object',
204
+ properties: {
205
+ path: {
206
+ type: 'string',
207
+ description: 'Relative path of the directory to delete (e.g. "docs/drafts").',
208
+ },
209
+ },
210
+ required: ['path'],
211
+ },
212
+ },
213
+ {
214
+ name: 'move_file',
215
+ description: 'Move or rename a file within the shared folder.',
216
+ inputSchema: {
217
+ type: 'object',
218
+ properties: {
219
+ from: {
220
+ type: 'string',
221
+ description: 'Relative path of the source file (e.g. "report.md" or "docs/draft.md").',
222
+ },
223
+ to: {
224
+ type: 'string',
225
+ description: 'Relative path of the destination (e.g. "archive/report.md").',
226
+ },
227
+ },
228
+ required: ['from', 'to'],
229
+ },
230
+ },
172
231
  ],
173
232
  };
174
233
  });
@@ -183,15 +242,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
183
242
  };
184
243
  }
185
244
  case 'list_shared_files': {
186
- const files = getSharedFiles();
187
- if (files.length === 0) {
245
+ const { path: relativePath } = z.object({ path: z.string().optional() }).parse(args);
246
+ const entries = getSharedFiles(relativePath ?? '');
247
+ if (entries.length === 0) {
248
+ const location = relativePath ? `"${relativePath}"` : 'the shared folder';
188
249
  return {
189
- content: [{ type: 'text', text: 'The shared folder is empty.' }],
250
+ content: [{ type: 'text', text: `${location} is empty.` }],
190
251
  };
191
252
  }
192
- const fileList = files.map(f => `- ${f.name} (${(f.size / 1024).toFixed(2)} KB)`).join('\n');
253
+ const dirs = entries.filter(e => e.type === 'directory').map(e => `📁 ${e.name}/`);
254
+ const files = entries.filter(e => e.type === 'file').map(e => `📄 ${e.name} (${((e.size ?? 0) / 1024).toFixed(2)} KB)`);
255
+ const list = [...dirs, ...files].join('\n');
256
+ const location = relativePath ? `"${relativePath}"` : 'shared folder';
193
257
  return {
194
- content: [{ type: 'text', text: `Files in shared folder:\n${fileList}` }],
258
+ content: [{ type: 'text', text: `Contents of ${location}:\n${list}` }],
195
259
  };
196
260
  }
197
261
  case 'share_file': {
@@ -201,18 +265,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
201
265
  };
202
266
  }
203
267
  case 'preview_file': {
204
- const { filename } = z.object({ filename: z.string() }).parse(args);
205
- const ext = path.extname(filename).toLowerCase();
268
+ const { path: relativePath } = z.object({ path: z.string() }).parse(args);
269
+ const ext = path.extname(relativePath).toLowerCase();
206
270
  if (!['.txt', '.md'].includes(ext)) {
207
271
  return {
208
272
  content: [{ type: 'text', text: `Error: Preview is only supported for .txt and .md files.` }],
209
273
  isError: true,
210
274
  };
211
275
  }
212
- const safePath = validateSafePath(filename);
276
+ const safePath = validateSafePath(relativePath);
213
277
  if (!fs.existsSync(safePath)) {
214
278
  return {
215
- content: [{ type: 'text', text: `Error: File "${filename}" not found in shared folder.` }],
279
+ content: [{ type: 'text', text: `Error: File "${relativePath}" not found in shared folder.` }],
216
280
  isError: true,
217
281
  };
218
282
  }
@@ -229,25 +293,48 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
229
293
  };
230
294
  }
231
295
  case 'write_file': {
232
- const { filename, content } = z.object({
296
+ const { filename, content, path: relativePath } = z.object({
233
297
  filename: z.string(),
234
298
  content: z.string(),
299
+ path: z.string().optional(),
235
300
  }).parse(args);
236
- const dest = writeSharedFile(filename, content);
301
+ const dest = writeSharedFile(filename, content, relativePath ?? '');
237
302
  return {
238
- content: [{ type: 'text', text: `File "${path.basename(dest)}" written to shared folder.` }],
303
+ content: [{ type: 'text', text: `File "${path.basename(dest)}" written to shared folder${relativePath ? ` (${relativePath})` : ''}.` }],
239
304
  };
240
305
  }
241
306
  case 'copy_file': {
242
- const { source_path, filename } = z.object({
307
+ const { source_path, filename, path: relativePath } = z.object({
243
308
  source_path: z.string(),
244
309
  filename: z.string().optional(),
310
+ path: z.string().optional(),
245
311
  }).parse(args);
246
- const copiedName = copyToSharedFolder(source_path, filename);
312
+ const copiedName = copyToSharedFolder(source_path, filename, relativePath ?? '');
247
313
  return {
248
314
  content: [{ type: 'text', text: `File "${copiedName}" copied to shared folder.` }],
249
315
  };
250
316
  }
317
+ case 'create_directory': {
318
+ const { path: dirPath } = z.object({ path: z.string() }).parse(args);
319
+ createDirectory(dirPath);
320
+ return {
321
+ content: [{ type: 'text', text: `Directory "${dirPath}" created successfully.` }],
322
+ };
323
+ }
324
+ case 'delete_directory': {
325
+ const { path: dirPath } = z.object({ path: z.string() }).parse(args);
326
+ deleteDirectory(dirPath);
327
+ return {
328
+ content: [{ type: 'text', text: `Directory "${dirPath}" deleted successfully.` }],
329
+ };
330
+ }
331
+ case 'move_file': {
332
+ const { from, to } = z.object({ from: z.string(), to: z.string() }).parse(args);
333
+ moveFile(from, to);
334
+ return {
335
+ content: [{ type: 'text', text: `File moved from "${from}" to "${to}".` }],
336
+ };
337
+ }
251
338
  default:
252
339
  throw new Error(`Tool not found: ${name}`);
253
340
  }
@@ -1,54 +1,83 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { getConfig } from './config-manager.js';
4
- export function getSharedFiles() {
4
+ export function validateSafePath(relativePath) {
5
5
  const { shared_folder_path } = getConfig();
6
- if (!fs.existsSync(shared_folder_path)) {
7
- fs.mkdirSync(shared_folder_path, { recursive: true });
6
+ const resolvedBase = path.resolve(shared_folder_path);
7
+ const fullPath = path.resolve(shared_folder_path, relativePath);
8
+ if (fullPath !== resolvedBase && !fullPath.startsWith(resolvedBase + path.sep)) {
9
+ throw new Error('Path traversal detected');
10
+ }
11
+ return fullPath;
12
+ }
13
+ export function getSharedFiles(relativePath = '') {
14
+ const dirPath = validateSafePath(relativePath);
15
+ if (!fs.existsSync(dirPath)) {
16
+ fs.mkdirSync(dirPath, { recursive: true });
8
17
  return [];
9
18
  }
10
- return fs.readdirSync(shared_folder_path).filter(file => {
11
- const filePath = path.join(shared_folder_path, file);
12
- return fs.statSync(filePath).isFile();
13
- }).map(file => {
14
- const filePath = path.join(shared_folder_path, file);
15
- const stats = fs.statSync(filePath);
19
+ return fs.readdirSync(dirPath).map(name => {
20
+ const fullPath = path.join(dirPath, name);
21
+ const stats = fs.statSync(fullPath);
22
+ if (stats.isDirectory()) {
23
+ return { name, type: 'directory', mtime: stats.mtime };
24
+ }
16
25
  return {
17
- name: file,
26
+ name,
27
+ type: 'file',
18
28
  size: stats.size,
19
29
  mtime: stats.mtime,
20
- extension: path.extname(file).toLowerCase(),
30
+ extension: path.extname(name).toLowerCase(),
21
31
  };
32
+ }).sort((a, b) => {
33
+ if (a.type !== b.type)
34
+ return a.type === 'directory' ? -1 : 1;
35
+ return a.name.localeCompare(b.name);
22
36
  });
23
37
  }
24
- export function validateSafePath(filename) {
25
- const { shared_folder_path } = getConfig();
26
- const safeName = path.basename(filename);
27
- const fullPath = path.join(shared_folder_path, safeName);
28
- // Extra safety check
29
- if (!fullPath.startsWith(path.resolve(shared_folder_path))) {
30
- throw new Error('Path traversal detected');
31
- }
32
- return fullPath;
33
- }
34
- export function deleteFile(filename) {
35
- const fullPath = validateSafePath(filename);
38
+ export function deleteFile(relativePath) {
39
+ const fullPath = validateSafePath(relativePath);
36
40
  if (fs.existsSync(fullPath)) {
37
41
  fs.unlinkSync(fullPath);
38
42
  }
39
43
  }
40
- export function writeSharedFile(filename, content) {
41
- const dest = validateSafePath(filename);
44
+ export function writeSharedFile(filename, content, relativePath = '') {
45
+ const dir = validateSafePath(relativePath);
46
+ fs.mkdirSync(dir, { recursive: true });
47
+ const dest = path.join(dir, path.basename(filename));
42
48
  fs.writeFileSync(dest, content, 'utf-8');
43
49
  return dest;
44
50
  }
45
- export function copyToSharedFolder(sourcePath, filename) {
46
- const targetFilename = filename ?? path.basename(sourcePath);
51
+ export function copyToSharedFolder(sourcePath, filename, relativePath = '') {
52
+ const targetFilename = filename ? path.basename(filename) : path.basename(sourcePath);
47
53
  const stats = fs.statSync(sourcePath);
48
54
  if (!stats.isFile()) {
49
55
  throw new Error(`Source path is not a file: ${sourcePath}`);
50
56
  }
51
- const dest = validateSafePath(targetFilename);
52
- fs.copyFileSync(sourcePath, dest);
53
- return targetFilename;
57
+ const dir = validateSafePath(relativePath);
58
+ fs.mkdirSync(dir, { recursive: true });
59
+ fs.copyFileSync(sourcePath, path.join(dir, targetFilename));
60
+ return relativePath ? `${relativePath}/${targetFilename}` : targetFilename;
61
+ }
62
+ export function createDirectory(relativePath) {
63
+ const fullPath = validateSafePath(relativePath);
64
+ fs.mkdirSync(fullPath, { recursive: true });
65
+ }
66
+ export function deleteDirectory(relativePath) {
67
+ const fullPath = validateSafePath(relativePath);
68
+ if (!fs.existsSync(fullPath))
69
+ throw new Error('Directory not found');
70
+ const contents = fs.readdirSync(fullPath);
71
+ if (contents.length > 0)
72
+ throw new Error('Directory is not empty');
73
+ fs.rmdirSync(fullPath);
74
+ }
75
+ export function moveFile(fromRelative, toRelative) {
76
+ const fromPath = validateSafePath(fromRelative);
77
+ const toPath = validateSafePath(toRelative);
78
+ if (!fs.existsSync(fromPath)) {
79
+ throw new Error(`File not found: ${fromRelative}`);
80
+ }
81
+ fs.mkdirSync(path.dirname(toPath), { recursive: true });
82
+ fs.renameSync(fromPath, toPath);
54
83
  }
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '1.0.2';
1
+ export const VERSION = '1.0.5';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "file-share-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.5",
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",