file-share-mcp 1.0.1
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 +154 -0
- package/dist/gui.js +235 -0
- package/dist/mcp-server.js +215 -0
- package/dist/utils/config-manager.js +58 -0
- package/dist/utils/file-manager.js +39 -0
- package/dist/utils/generate-version.js +10 -0
- package/dist/utils/logger.js +45 -0
- package/dist/version.js +1 -0
- package/package.json +57 -0
- package/public/assets/index-CD6a3a8W.js +69 -0
- package/public/assets/index-hXhXe8uC.css +1 -0
- package/public/index.html +14 -0
- package/public/vite.svg +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# File Share MCP
|
|
2
|
+
|
|
3
|
+
An MCP server for bidirectional file sharing between AI agents and users, with a built-in web GUI.
|
|
4
|
+
|
|
5
|
+
The agent can list, read, and notify about files. The user can upload, download, and delete files via the GUI — all through a shared local folder.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **MCP Tools** for the AI agent:
|
|
10
|
+
- `get_shared_folder` — Get the current shared folder path
|
|
11
|
+
- `list_shared_files` — List all files available in the shared folder
|
|
12
|
+
- `share_file` — Notify the user that a file is ready to download
|
|
13
|
+
- `preview_file` — Read the content of a `.txt` or `.md` file (max 1 MB)
|
|
14
|
+
- **Web GUI** — Upload, download, preview, and delete files from a browser
|
|
15
|
+
- **Zero config** — works out of the box with sensible defaults (`~/.file-share-mcp`)
|
|
16
|
+
- **Proxy-ready** — optional integration with the [file-share-mcp proxy](https://github.com/...)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Quick Start with Cursor
|
|
21
|
+
|
|
22
|
+
Add this to your Cursor MCP configuration (`~/.cursor/mcp.json` or your workspace `.cursor/mcp.json`):
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"file-share": {
|
|
28
|
+
"command": "npx",
|
|
29
|
+
"args": ["-y", "file-share-mcp"]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Once added, restart Cursor. The GUI will be available at **http://localhost:3005**.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Quick Start with Claude Desktop
|
|
40
|
+
|
|
41
|
+
Add this to your Claude Desktop configuration (`claude_desktop_config.json`):
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"file-share": {
|
|
47
|
+
"command": "npx",
|
|
48
|
+
"args": ["-y", "file-share-mcp"]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Configuration (optional)
|
|
57
|
+
|
|
58
|
+
All configuration is done via environment variables, passed in the MCP `env` block or in a `.env` file inside the package directory.
|
|
59
|
+
|
|
60
|
+
| Variable | Default | Description |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `GUI_PORT` | `3005` | Port for the web GUI |
|
|
63
|
+
| `MCP_DATA_DIR` | `~/.file-share-mcp` | Directory for `configs.json` and persistent data |
|
|
64
|
+
| `PROXY_URL` | _(none)_ | URL of the proxy server (optional) |
|
|
65
|
+
| `PROXY_APP_PATH` | `/file-share` | Path to register on the proxy |
|
|
66
|
+
| `PROXY_APP_NAME` | `File Share MCP` | Display name shown in the proxy dashboard |
|
|
67
|
+
|
|
68
|
+
### Example: custom port and data directory
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"file-share": {
|
|
74
|
+
"command": "npx",
|
|
75
|
+
"args": ["-y", "file-share-mcp"],
|
|
76
|
+
"env": {
|
|
77
|
+
"GUI_PORT": "3010",
|
|
78
|
+
"MCP_DATA_DIR": "C:/Users/YourName/my-shared-files"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Example: with proxy integration
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"mcpServers": {
|
|
90
|
+
"file-share": {
|
|
91
|
+
"command": "npx",
|
|
92
|
+
"args": ["-y", "file-share-mcp"],
|
|
93
|
+
"env": {
|
|
94
|
+
"PROXY_URL": "http://localhost:3000",
|
|
95
|
+
"PROXY_APP_PATH": "/file-share",
|
|
96
|
+
"PROXY_APP_NAME": "File Share MCP"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Data & Logs
|
|
106
|
+
|
|
107
|
+
By default, all user data is stored in `~/.file-share-mcp/`:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
~/.file-share-mcp/
|
|
111
|
+
├── configs.json # Saved configuration (shared folder path, etc.)
|
|
112
|
+
├── shared/ # Default shared folder
|
|
113
|
+
└── logs/
|
|
114
|
+
└── server.log # Application logs
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This directory survives package updates and reinstalls.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Install dependencies
|
|
125
|
+
npm install
|
|
126
|
+
cd gui && npm install && cd ..
|
|
127
|
+
|
|
128
|
+
# Build (backend + frontend)
|
|
129
|
+
npm run build
|
|
130
|
+
|
|
131
|
+
# Start
|
|
132
|
+
npm start
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Project structure
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
src/
|
|
139
|
+
├── mcp-server.ts # MCP server (MASTER process)
|
|
140
|
+
├── gui.ts # Express GUI server (WORKER process)
|
|
141
|
+
└── utils/
|
|
142
|
+
├── config-manager.ts
|
|
143
|
+
├── file-manager.ts
|
|
144
|
+
├── logger.ts
|
|
145
|
+
└── generate-version.ts
|
|
146
|
+
gui/ # React + Vite frontend (builds to ../public/)
|
|
147
|
+
public/ # Built frontend (served by the GUI server)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
ISC
|
package/dist/gui.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import multer from 'multer';
|
|
3
|
+
import cors from 'cors';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import axios from 'axios';
|
|
8
|
+
import { setupLogging } from './utils/logger.js';
|
|
9
|
+
import { getConfig, saveConfig } from './utils/config-manager.js';
|
|
10
|
+
import { getSharedFiles, validateSafePath, deleteFile } from './utils/file-manager.js';
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
setupLogging('GUI');
|
|
14
|
+
const app = express();
|
|
15
|
+
app.use(cors());
|
|
16
|
+
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 });
|
|
33
|
+
// Endpoints
|
|
34
|
+
app.get('/proxy/health', (req, res) => {
|
|
35
|
+
res.json({ status: 'healthy' });
|
|
36
|
+
});
|
|
37
|
+
app.get('/api/files', (req, res) => {
|
|
38
|
+
try {
|
|
39
|
+
const files = getSharedFiles();
|
|
40
|
+
res.json(files);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
res.status(500).json({ error: error.message });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
app.get('/api/download/:filename', (req, res) => {
|
|
47
|
+
try {
|
|
48
|
+
const safePath = validateSafePath(req.params.filename);
|
|
49
|
+
if (fs.existsSync(safePath)) {
|
|
50
|
+
res.download(safePath);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
res.status(404).send('File not found');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
res.status(400).send(error.message);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
app.delete('/api/files/:filename', (req, res) => {
|
|
61
|
+
try {
|
|
62
|
+
deleteFile(req.params.filename);
|
|
63
|
+
res.json({ message: 'File deleted successfully' });
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
res.status(400).json({ error: error.message });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
const PREVIEWABLE_EXTENSIONS = ['.txt', '.md'];
|
|
70
|
+
const PREVIEW_MAX_SIZE = 1024 * 1024; // 1 MB
|
|
71
|
+
app.get('/api/preview/:filename', (req, res) => {
|
|
72
|
+
try {
|
|
73
|
+
const ext = path.extname(req.params.filename).toLowerCase();
|
|
74
|
+
if (!PREVIEWABLE_EXTENSIONS.includes(ext)) {
|
|
75
|
+
return res.status(400).json({ error: 'Preview not supported for this file type' });
|
|
76
|
+
}
|
|
77
|
+
const safePath = validateSafePath(req.params.filename);
|
|
78
|
+
if (!fs.existsSync(safePath)) {
|
|
79
|
+
return res.status(404).json({ error: 'File not found' });
|
|
80
|
+
}
|
|
81
|
+
const stats = fs.statSync(safePath);
|
|
82
|
+
if (stats.size > PREVIEW_MAX_SIZE) {
|
|
83
|
+
return res.status(400).json({ error: 'File too large to preview (max 1 MB)' });
|
|
84
|
+
}
|
|
85
|
+
const content = fs.readFileSync(safePath, 'utf-8');
|
|
86
|
+
res.json({ content, type: ext === '.md' ? 'markdown' : 'text' });
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
res.status(400).json({ error: error.message });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
app.post('/api/upload', upload.single('file'), (req, res) => {
|
|
93
|
+
if (req.file) {
|
|
94
|
+
res.json({ message: 'File uploaded successfully', filename: req.file.filename });
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
res.status(400).json({ error: 'No file uploaded' });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
app.get('/api/config', (req, res) => {
|
|
101
|
+
const config = getConfig();
|
|
102
|
+
res.json({
|
|
103
|
+
shared_folder_path: config.shared_folder_path,
|
|
104
|
+
mcp_data_dir: config.MCP_DATA_DIR
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
app.post('/api/config', (req, res) => {
|
|
108
|
+
try {
|
|
109
|
+
const { shared_folder_path } = req.body;
|
|
110
|
+
if (!shared_folder_path) {
|
|
111
|
+
return res.status(400).json({ error: 'shared_folder_path is required' });
|
|
112
|
+
}
|
|
113
|
+
saveConfig({ shared_folder_path });
|
|
114
|
+
res.json({ message: 'Configuration updated successfully' });
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
res.status(500).json({ error: error.message });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
// GET /api/configurations — Convention méta-config proxy
|
|
121
|
+
app.get('/api/configurations', (req, res) => {
|
|
122
|
+
const config = getConfig();
|
|
123
|
+
res.json({
|
|
124
|
+
configurations: [{
|
|
125
|
+
id: 'default',
|
|
126
|
+
name: 'Configuration actuelle',
|
|
127
|
+
description: `Dossier partagé: ${config.shared_folder_path}`,
|
|
128
|
+
active: true
|
|
129
|
+
}]
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
// POST /api/configurations/apply — Convention méta-config proxy
|
|
133
|
+
app.post('/api/configurations/apply', (req, res) => {
|
|
134
|
+
const { configId } = req.body;
|
|
135
|
+
if (configId === 'default') {
|
|
136
|
+
return res.json({ success: true, appliedConfig: 'default' });
|
|
137
|
+
}
|
|
138
|
+
return res.status(404).json({ success: false, error: `Configuration "${configId}" not found` });
|
|
139
|
+
});
|
|
140
|
+
// SPA Support fallback
|
|
141
|
+
const publicDir = path.resolve(__dirname, '../public');
|
|
142
|
+
app.use(express.static(publicDir));
|
|
143
|
+
app.get(/^(?!\/api\/|\/proxy\/)/, (req, res) => {
|
|
144
|
+
const indexHtml = path.join(publicDir, 'index.html');
|
|
145
|
+
if (fs.existsSync(indexHtml)) {
|
|
146
|
+
res.sendFile(indexHtml);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
res.status(404).send('GUI not built yet. Run build process.');
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
// Proxy registration & server startup
|
|
153
|
+
const config = getConfig();
|
|
154
|
+
async function registerWithProxy() {
|
|
155
|
+
if (!config.PROXY_URL || !config.PROXY_APP_PATH)
|
|
156
|
+
return null;
|
|
157
|
+
try {
|
|
158
|
+
// Check if proxy is up
|
|
159
|
+
await axios.get(`${config.PROXY_URL}/proxy/health`, { timeout: 2000 });
|
|
160
|
+
const response = await axios.post(`${config.PROXY_URL}/proxy/register`, {
|
|
161
|
+
path: config.PROXY_APP_PATH,
|
|
162
|
+
name: config.PROXY_APP_NAME
|
|
163
|
+
}, { timeout: 2000 });
|
|
164
|
+
const assignedPort = response.data.port;
|
|
165
|
+
console.log(`Successfully registered with proxy at ${config.PROXY_URL}${config.PROXY_APP_PATH} (port ${assignedPort})`);
|
|
166
|
+
return assignedPort;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
console.error(`Failed to register with proxy: ${error.message}`);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function unregisterFromProxy() {
|
|
174
|
+
if (!config.PROXY_URL || !config.PROXY_APP_PATH)
|
|
175
|
+
return;
|
|
176
|
+
try {
|
|
177
|
+
await axios.delete(`${config.PROXY_URL}/proxy/unregister`, {
|
|
178
|
+
data: { path: config.PROXY_APP_PATH },
|
|
179
|
+
timeout: 2000
|
|
180
|
+
});
|
|
181
|
+
console.log(`Unregistered from proxy`);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
// Ignore error on shutdown
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
let server;
|
|
188
|
+
async function startServer() {
|
|
189
|
+
let port;
|
|
190
|
+
if (config.PROXY_URL) {
|
|
191
|
+
const assignedPort = await registerWithProxy();
|
|
192
|
+
if (assignedPort) {
|
|
193
|
+
port = assignedPort;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
console.error(`Could not register with proxy. Falling back to standalone mode on port ${config.GUI_PORT}.`);
|
|
197
|
+
port = config.GUI_PORT;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
port = config.GUI_PORT;
|
|
202
|
+
}
|
|
203
|
+
server = app.listen(port, () => {
|
|
204
|
+
console.log(`GUI API server listening on port ${port}`);
|
|
205
|
+
});
|
|
206
|
+
server.on('error', (err) => {
|
|
207
|
+
if (err.code === 'EADDRINUSE') {
|
|
208
|
+
console.error(`PORT CONFLICT: Port ${port} is already in use.`);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
startServer();
|
|
213
|
+
// IPC and signals
|
|
214
|
+
process.on('message', async (msg) => {
|
|
215
|
+
if (msg === 'STOP') {
|
|
216
|
+
console.log('GUI received STOP signal');
|
|
217
|
+
await unregisterFromProxy();
|
|
218
|
+
server?.close(() => process.exit(0));
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
process.on('error', (err) => {
|
|
222
|
+
if (err.code === 'ERR_IPC_CHANNEL_CLOSED') {
|
|
223
|
+
console.error('[IPC] Channel closed (expected on disconnect).');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
console.error(`[Process] Error: ${err.message}`);
|
|
227
|
+
});
|
|
228
|
+
process.on('disconnect', () => {
|
|
229
|
+
console.error('Disconnected from parent, exiting.');
|
|
230
|
+
process.exit(0);
|
|
231
|
+
});
|
|
232
|
+
process.on('SIGTERM', async () => {
|
|
233
|
+
await unregisterFromProxy();
|
|
234
|
+
process.exit(0);
|
|
235
|
+
});
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { fork } from 'child_process';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import { setupLogging } from './utils/logger.js';
|
|
11
|
+
import { getConfig } from './utils/config-manager.js';
|
|
12
|
+
import { getSharedFiles, validateSafePath } from './utils/file-manager.js';
|
|
13
|
+
import { VERSION } from './version.js';
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
setupLogging('MASTER');
|
|
17
|
+
const server = new Server({
|
|
18
|
+
name: 'file-share-mcp',
|
|
19
|
+
version: VERSION,
|
|
20
|
+
}, {
|
|
21
|
+
capabilities: {
|
|
22
|
+
tools: {},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
let guiProcess = null;
|
|
26
|
+
/**
|
|
27
|
+
* Envoi IPC robuste — vérifie que le canal est connecté avant d'envoyer
|
|
28
|
+
*/
|
|
29
|
+
function safeSend(proc, message) {
|
|
30
|
+
if (proc && typeof proc.send === 'function' && proc.connected) {
|
|
31
|
+
try {
|
|
32
|
+
return proc.send(message);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
console.error(`[IPC] Failed to send message: ${err.message}`);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
function startGui() {
|
|
42
|
+
if (guiProcess) {
|
|
43
|
+
guiProcess.kill();
|
|
44
|
+
}
|
|
45
|
+
const guiPath = path.join(__dirname, 'gui.js');
|
|
46
|
+
guiProcess = fork(guiPath, [], {
|
|
47
|
+
stdio: ['ignore', 'pipe', 'inherit', 'ipc'],
|
|
48
|
+
env: { ...process.env }
|
|
49
|
+
});
|
|
50
|
+
// Redirect GUI stdout to stderr to protect JSON-RPC
|
|
51
|
+
guiProcess.stdout?.on('data', (data) => {
|
|
52
|
+
process.stderr.write(data);
|
|
53
|
+
});
|
|
54
|
+
guiProcess.on('message', (message) => {
|
|
55
|
+
if (message === 'RESTART') {
|
|
56
|
+
console.log('GUI requested restart...');
|
|
57
|
+
startGui();
|
|
58
|
+
}
|
|
59
|
+
else if (message === 'STOP') {
|
|
60
|
+
console.log('GUI requested stop...');
|
|
61
|
+
cleanup().then(() => process.exit(0));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
guiProcess.on('exit', (code) => {
|
|
65
|
+
if (code !== 0 && code !== null) {
|
|
66
|
+
console.error(`GUI process exited with code ${code}. Restarting...`);
|
|
67
|
+
setTimeout(startGui, 2000);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async function cleanup() {
|
|
72
|
+
console.log('Cleaning up...');
|
|
73
|
+
if (guiProcess) {
|
|
74
|
+
safeSend(guiProcess, 'STOP');
|
|
75
|
+
// Give it a moment to stop gracefully
|
|
76
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
77
|
+
guiProcess.kill();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
process.on('SIGINT', async () => {
|
|
81
|
+
await cleanup();
|
|
82
|
+
process.exit(0);
|
|
83
|
+
});
|
|
84
|
+
process.on('SIGTERM', async () => {
|
|
85
|
+
await cleanup();
|
|
86
|
+
process.exit(0);
|
|
87
|
+
});
|
|
88
|
+
// Tool Definitions
|
|
89
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
90
|
+
return {
|
|
91
|
+
tools: [
|
|
92
|
+
{
|
|
93
|
+
name: 'get_shared_folder',
|
|
94
|
+
description: 'Get the current path of the shared folder where files are stored.',
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'list_shared_files',
|
|
102
|
+
description: 'List all files currently available in the shared folder.',
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'share_file',
|
|
110
|
+
description: 'Notify the user that a file is available in the shared folder. This tool does not move files, it just informs the user. IMPORTANT: You must provide the ABSOLUTE path to the file.',
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: {
|
|
114
|
+
filename: {
|
|
115
|
+
type: 'string',
|
|
116
|
+
description: 'The absolute path of the file to share.',
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
required: ['filename'],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'preview_file',
|
|
124
|
+
description: 'Preview the content of a .txt or .md (Markdown) file from the shared folder. Returns the raw text content (max 1 MB).',
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
properties: {
|
|
128
|
+
filename: {
|
|
129
|
+
type: 'string',
|
|
130
|
+
description: 'The name of the file to preview (filename only, not the full path).',
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
required: ['filename'],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
140
|
+
const { name, arguments: args } = request.params;
|
|
141
|
+
try {
|
|
142
|
+
switch (name) {
|
|
143
|
+
case 'get_shared_folder': {
|
|
144
|
+
const config = getConfig();
|
|
145
|
+
return {
|
|
146
|
+
content: [{ type: 'text', text: `Shared folder path: ${config.shared_folder_path}` }],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
case 'list_shared_files': {
|
|
150
|
+
const files = getSharedFiles();
|
|
151
|
+
if (files.length === 0) {
|
|
152
|
+
return {
|
|
153
|
+
content: [{ type: 'text', text: 'The shared folder is empty.' }],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const fileList = files.map(f => `- ${f.name} (${(f.size / 1024).toFixed(2)} KB)`).join('\n');
|
|
157
|
+
return {
|
|
158
|
+
content: [{ type: 'text', text: `Files in shared folder:\n${fileList}` }],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
case 'share_file': {
|
|
162
|
+
const { filename } = z.object({ filename: z.string() }).parse(args);
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: 'text', text: `File "${filename}" has been shared and is visible in the GUI.` }],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
case 'preview_file': {
|
|
168
|
+
const { filename } = z.object({ filename: z.string() }).parse(args);
|
|
169
|
+
const ext = path.extname(filename).toLowerCase();
|
|
170
|
+
if (!['.txt', '.md'].includes(ext)) {
|
|
171
|
+
return {
|
|
172
|
+
content: [{ type: 'text', text: `Error: Preview is only supported for .txt and .md files.` }],
|
|
173
|
+
isError: true,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const safePath = validateSafePath(filename);
|
|
177
|
+
if (!fs.existsSync(safePath)) {
|
|
178
|
+
return {
|
|
179
|
+
content: [{ type: 'text', text: `Error: File "${filename}" not found in shared folder.` }],
|
|
180
|
+
isError: true,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const stats = fs.statSync(safePath);
|
|
184
|
+
if (stats.size > 1024 * 1024) {
|
|
185
|
+
return {
|
|
186
|
+
content: [{ type: 'text', text: `Error: File too large to preview (max 1 MB).` }],
|
|
187
|
+
isError: true,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const content = fs.readFileSync(safePath, 'utf-8');
|
|
191
|
+
return {
|
|
192
|
+
content: [{ type: 'text', text: content }],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
default:
|
|
196
|
+
throw new Error(`Tool not found: ${name}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
return {
|
|
201
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
202
|
+
isError: true,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
async function main() {
|
|
207
|
+
const transport = new StdioServerTransport();
|
|
208
|
+
await server.connect(transport);
|
|
209
|
+
console.log('File Share MCP Server started');
|
|
210
|
+
startGui();
|
|
211
|
+
}
|
|
212
|
+
main().catch((error) => {
|
|
213
|
+
console.error('Fatal error in main():', error);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const rootDir = path.resolve(__dirname, '../../');
|
|
9
|
+
const envPath = path.join(rootDir, '.env');
|
|
10
|
+
// User-level base dir that survives package updates
|
|
11
|
+
const USER_BASE_DIR = path.join(os.homedir(), '.file-share-mcp');
|
|
12
|
+
const DEFAULT_CONFIG = {
|
|
13
|
+
shared_folder_path: path.join(USER_BASE_DIR, 'shared'),
|
|
14
|
+
};
|
|
15
|
+
export function getConfig() {
|
|
16
|
+
dotenv.config({ path: envPath, override: true });
|
|
17
|
+
const dataDirEnv = process.env.MCP_DATA_DIR;
|
|
18
|
+
const dataDir = dataDirEnv
|
|
19
|
+
? (path.isAbsolute(dataDirEnv) ? dataDirEnv : path.resolve(rootDir, dataDirEnv))
|
|
20
|
+
: USER_BASE_DIR;
|
|
21
|
+
if (!fs.existsSync(dataDir)) {
|
|
22
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
const configsPath = path.join(dataDir, 'configs.json');
|
|
25
|
+
let currentConfig = { ...DEFAULT_CONFIG };
|
|
26
|
+
if (fs.existsSync(configsPath)) {
|
|
27
|
+
try {
|
|
28
|
+
const saved = JSON.parse(fs.readFileSync(configsPath, 'utf-8'));
|
|
29
|
+
currentConfig = { ...currentConfig, ...saved };
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
console.error('Failed to parse configs.json:', err);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
fs.writeFileSync(configsPath, JSON.stringify(currentConfig, null, 2));
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
...currentConfig,
|
|
40
|
+
GUI_PORT: parseInt(process.env.GUI_PORT || '3005', 10),
|
|
41
|
+
PROXY_URL: process.env.PROXY_URL,
|
|
42
|
+
PROXY_APP_PATH: process.env.PROXY_APP_PATH || '/file-share',
|
|
43
|
+
PROXY_APP_NAME: process.env.PROXY_APP_NAME || 'File Share MCP',
|
|
44
|
+
MCP_DATA_DIR: dataDir,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function saveConfig(newConfig) {
|
|
48
|
+
const fullConfig = getConfig();
|
|
49
|
+
const configsPath = path.join(fullConfig.MCP_DATA_DIR, 'configs.json');
|
|
50
|
+
const configToSave = {
|
|
51
|
+
shared_folder_path: newConfig.shared_folder_path || fullConfig.shared_folder_path,
|
|
52
|
+
};
|
|
53
|
+
fs.writeFileSync(configsPath, JSON.stringify(configToSave, null, 2));
|
|
54
|
+
// Ensure shared folder exists
|
|
55
|
+
if (!fs.existsSync(configToSave.shared_folder_path)) {
|
|
56
|
+
fs.mkdirSync(configToSave.shared_folder_path, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getConfig } from './config-manager.js';
|
|
4
|
+
export function getSharedFiles() {
|
|
5
|
+
const { shared_folder_path } = getConfig();
|
|
6
|
+
if (!fs.existsSync(shared_folder_path)) {
|
|
7
|
+
fs.mkdirSync(shared_folder_path, { recursive: true });
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
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);
|
|
16
|
+
return {
|
|
17
|
+
name: file,
|
|
18
|
+
size: stats.size,
|
|
19
|
+
mtime: stats.mtime,
|
|
20
|
+
extension: path.extname(file).toLowerCase(),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
}
|
|
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);
|
|
36
|
+
if (fs.existsSync(fullPath)) {
|
|
37
|
+
fs.unlinkSync(fullPath);
|
|
38
|
+
}
|
|
39
|
+
}
|