easysend-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 +54 -0
- package/index.js +340 -0
- package/package.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# easysend-mcp
|
|
2
|
+
|
|
3
|
+
EasySend file sharing plugin for Claude Code. Upload, download, and share files directly from your AI assistant session.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Add to your Claude Code settings (`~/.claude/settings.json`):
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"easysend": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["easysend-mcp"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or install globally:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g easysend-mcp
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Tools
|
|
27
|
+
|
|
28
|
+
| Tool | Description |
|
|
29
|
+
|------|-------------|
|
|
30
|
+
| `upload_files` | Upload files and get a share link |
|
|
31
|
+
| `get_bundle` | Get bundle info (files, sizes, expiry) |
|
|
32
|
+
| `download_file` | Download a file by ID |
|
|
33
|
+
| `download_bundle` | Download all files in a bundle |
|
|
34
|
+
| `delete_file` | Delete a file (requires upload_token) |
|
|
35
|
+
| `bundle_status` | Quick status check |
|
|
36
|
+
|
|
37
|
+
## Usage Examples
|
|
38
|
+
|
|
39
|
+
In Claude Code, just ask naturally:
|
|
40
|
+
|
|
41
|
+
- "Upload report.pdf to EasySend"
|
|
42
|
+
- "Share all the CSV files in this directory"
|
|
43
|
+
- "What files are in easysend.co/Ab3Kz?"
|
|
44
|
+
- "Download the files from that EasySend bundle"
|
|
45
|
+
|
|
46
|
+
## No API Key Required
|
|
47
|
+
|
|
48
|
+
EasySend's API is free and public. No authentication needed for uploads.
|
|
49
|
+
|
|
50
|
+
## Links
|
|
51
|
+
|
|
52
|
+
- Website: https://easysend.co
|
|
53
|
+
- API Docs: https://easysend.co/api
|
|
54
|
+
- MCP Docs: https://easysend.co/mcp
|
package/index.js
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* EasySend MCP Server - File sharing tools for Claude Code
|
|
5
|
+
* Upload, download, and share files via easysend.co
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
9
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
|
+
import {
|
|
11
|
+
CallToolRequestSchema,
|
|
12
|
+
ListToolsRequestSchema,
|
|
13
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
14
|
+
import { readFile, writeFile, stat } from 'node:fs/promises';
|
|
15
|
+
import { basename, resolve, join } from 'node:path';
|
|
16
|
+
import { createReadStream } from 'node:fs';
|
|
17
|
+
|
|
18
|
+
const API = 'https://easysend.co/api/v1';
|
|
19
|
+
|
|
20
|
+
// --- Tool definitions ---
|
|
21
|
+
const TOOLS = [
|
|
22
|
+
{
|
|
23
|
+
name: 'upload_files',
|
|
24
|
+
description: 'Upload one or more files to EasySend and get a shareable link. No authentication required. Accepts any file type up to 1GB total.',
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
files: {
|
|
29
|
+
type: 'array',
|
|
30
|
+
items: { type: 'string' },
|
|
31
|
+
description: 'Array of absolute file paths to upload',
|
|
32
|
+
},
|
|
33
|
+
description: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: 'Optional note/description visible to recipients',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
required: ['files'],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'get_bundle',
|
|
43
|
+
description: 'Get information about an EasySend bundle - file list, sizes, download counts, expiry time.',
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
short_code: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: 'The bundle code from the share URL (e.g. "Ab3Kz" from easysend.co/Ab3Kz)',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
required: ['short_code'],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'download_file',
|
|
57
|
+
description: 'Download a single file from an EasySend bundle by file ID and save it to disk.',
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
file_id: { type: 'number', description: 'The file ID from bundle info' },
|
|
62
|
+
save_path: { type: 'string', description: 'Optional path to save the file (default: current directory with original name)' },
|
|
63
|
+
},
|
|
64
|
+
required: ['file_id'],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'download_bundle',
|
|
69
|
+
description: 'Download ALL files from an EasySend bundle to a local directory.',
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
short_code: { type: 'string', description: 'The bundle code' },
|
|
74
|
+
save_dir: { type: 'string', description: 'Directory to save files (default: current directory)' },
|
|
75
|
+
},
|
|
76
|
+
required: ['short_code'],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'delete_file',
|
|
81
|
+
description: 'Delete a file from an EasySend bundle. Requires the upload_token returned during upload.',
|
|
82
|
+
inputSchema: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: {
|
|
85
|
+
file_id: { type: 'number', description: 'The file ID to delete' },
|
|
86
|
+
upload_token: { type: 'string', description: 'The upload token for authorization' },
|
|
87
|
+
},
|
|
88
|
+
required: ['file_id', 'upload_token'],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'bundle_status',
|
|
93
|
+
description: 'Quick status check on an EasySend bundle - is it alive or expired, space remaining.',
|
|
94
|
+
inputSchema: {
|
|
95
|
+
type: 'object',
|
|
96
|
+
properties: {
|
|
97
|
+
short_code: { type: 'string', description: 'The bundle code' },
|
|
98
|
+
},
|
|
99
|
+
required: ['short_code'],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
// --- Tool handlers ---
|
|
105
|
+
|
|
106
|
+
async function handleUpload(args) {
|
|
107
|
+
const { files, description } = args;
|
|
108
|
+
|
|
109
|
+
if (!files || files.length === 0) {
|
|
110
|
+
return { content: [{ type: 'text', text: 'Error: No files specified.' }] };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Validate files exist
|
|
114
|
+
for (const f of files) {
|
|
115
|
+
try {
|
|
116
|
+
await stat(resolve(f));
|
|
117
|
+
} catch {
|
|
118
|
+
return { content: [{ type: 'text', text: `Error: File not found: ${f}` }] };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Build multipart form data manually
|
|
123
|
+
const boundary = '----EasySendMCP' + Date.now();
|
|
124
|
+
const parts = [];
|
|
125
|
+
|
|
126
|
+
for (const filePath of files) {
|
|
127
|
+
const absPath = resolve(filePath);
|
|
128
|
+
const name = basename(absPath);
|
|
129
|
+
const data = await readFile(absPath);
|
|
130
|
+
parts.push(
|
|
131
|
+
`--${boundary}\r\n` +
|
|
132
|
+
`Content-Disposition: form-data; name="files[]"; filename="${name}"\r\n` +
|
|
133
|
+
`Content-Type: application/octet-stream\r\n\r\n`
|
|
134
|
+
);
|
|
135
|
+
parts.push(data);
|
|
136
|
+
parts.push('\r\n');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (description) {
|
|
140
|
+
parts.push(
|
|
141
|
+
`--${boundary}\r\n` +
|
|
142
|
+
`Content-Disposition: form-data; name="description"\r\n\r\n` +
|
|
143
|
+
description + '\r\n'
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
parts.push(`--${boundary}--\r\n`);
|
|
148
|
+
|
|
149
|
+
// Combine parts into a single buffer
|
|
150
|
+
const buffers = parts.map(p => typeof p === 'string' ? Buffer.from(p) : p);
|
|
151
|
+
const body = Buffer.concat(buffers);
|
|
152
|
+
|
|
153
|
+
const response = await fetch(`${API}/upload`, {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` },
|
|
156
|
+
body,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const data = await response.json();
|
|
160
|
+
|
|
161
|
+
if (!data.success) {
|
|
162
|
+
return { content: [{ type: 'text', text: `Upload failed: ${data.error || 'Unknown error'}` }] };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const shareUrl = `https://easysend.co/${data.short_code}`;
|
|
166
|
+
const text = [
|
|
167
|
+
`Uploaded ${files.length} file(s)!`,
|
|
168
|
+
`Share URL: ${shareUrl}`,
|
|
169
|
+
`Upload token: ${data.upload_token}`,
|
|
170
|
+
`Expires: ${data.expires_at || '3 days'}`,
|
|
171
|
+
].join('\n');
|
|
172
|
+
|
|
173
|
+
return { content: [{ type: 'text', text }] };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function handleGetBundle(args) {
|
|
177
|
+
const { short_code } = args;
|
|
178
|
+
const response = await fetch(`${API}/bundle/${encodeURIComponent(short_code)}`);
|
|
179
|
+
const data = await response.json();
|
|
180
|
+
|
|
181
|
+
if (!data.success) {
|
|
182
|
+
return { content: [{ type: 'text', text: `Error: ${data.error || 'Bundle not found'}` }] };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const lines = [
|
|
186
|
+
`Bundle: ${data.short_code}`,
|
|
187
|
+
`Files: ${data.file_count}`,
|
|
188
|
+
`Total size: ${formatSize(data.total_size_bytes)}`,
|
|
189
|
+
`Expires: ${data.expires_at}`,
|
|
190
|
+
`Expired: ${data.is_expired ? 'Yes' : 'No'}`,
|
|
191
|
+
`Encrypted: ${data.is_encrypted ? 'Yes' : 'No'}`,
|
|
192
|
+
`Views: ${data.view_count}`,
|
|
193
|
+
data.description ? `Note: ${data.description}` : null,
|
|
194
|
+
'',
|
|
195
|
+
'Files:',
|
|
196
|
+
...data.files.map((f, i) =>
|
|
197
|
+
` ${i + 1}. ${f.name} (${formatSize(f.size)}) - ID: ${f.id}, Downloads: ${f.download_count}`
|
|
198
|
+
),
|
|
199
|
+
].filter(Boolean);
|
|
200
|
+
|
|
201
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function handleDownloadFile(args) {
|
|
205
|
+
const { file_id, save_path } = args;
|
|
206
|
+
|
|
207
|
+
// First get file info from a bundle lookup isn't needed - just download
|
|
208
|
+
const response = await fetch(`${API}/download/${file_id}`);
|
|
209
|
+
|
|
210
|
+
if (!response.ok) {
|
|
211
|
+
return { content: [{ type: 'text', text: `Error: File not found or expired (HTTP ${response.status})` }] };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Get filename from Content-Disposition header
|
|
215
|
+
const disposition = response.headers.get('content-disposition') || '';
|
|
216
|
+
const match = disposition.match(/filename="([^"]+)"/);
|
|
217
|
+
const fileName = match ? match[1] : `file_${file_id}`;
|
|
218
|
+
|
|
219
|
+
const saveTo = save_path ? resolve(save_path) : resolve(fileName);
|
|
220
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
221
|
+
await writeFile(saveTo, buffer);
|
|
222
|
+
|
|
223
|
+
return { content: [{ type: 'text', text: `Downloaded: ${saveTo} (${formatSize(buffer.length)})` }] };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function handleDownloadBundle(args) {
|
|
227
|
+
const { short_code, save_dir } = args;
|
|
228
|
+
|
|
229
|
+
// Get bundle info
|
|
230
|
+
const infoRes = await fetch(`${API}/bundle/${encodeURIComponent(short_code)}`);
|
|
231
|
+
const info = await infoRes.json();
|
|
232
|
+
|
|
233
|
+
if (!info.success) {
|
|
234
|
+
return { content: [{ type: 'text', text: `Error: ${info.error || 'Bundle not found'}` }] };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (info.is_encrypted) {
|
|
238
|
+
return { content: [{ type: 'text', text: 'Error: Cannot download encrypted bundles - files must be decrypted in the browser.' }] };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const dir = save_dir ? resolve(save_dir) : process.cwd();
|
|
242
|
+
const savedFiles = [];
|
|
243
|
+
|
|
244
|
+
for (const file of info.files) {
|
|
245
|
+
const response = await fetch(`${API}/download/${file.id}`);
|
|
246
|
+
if (!response.ok) continue;
|
|
247
|
+
|
|
248
|
+
const saveTo = join(dir, file.name);
|
|
249
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
250
|
+
await writeFile(saveTo, buffer);
|
|
251
|
+
savedFiles.push(saveTo);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
content: [{
|
|
256
|
+
type: 'text',
|
|
257
|
+
text: `Downloaded ${savedFiles.length} file(s) to ${dir}:\n` +
|
|
258
|
+
savedFiles.map(f => ` - ${f}`).join('\n'),
|
|
259
|
+
}],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function handleDeleteFile(args) {
|
|
264
|
+
const { file_id, upload_token } = args;
|
|
265
|
+
|
|
266
|
+
const response = await fetch(`${API}/file/${file_id}`, {
|
|
267
|
+
method: 'DELETE',
|
|
268
|
+
headers: { 'Authorization': `Bearer ${upload_token}` },
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const data = await response.json();
|
|
272
|
+
|
|
273
|
+
if (!data.success) {
|
|
274
|
+
return { content: [{ type: 'text', text: `Error: ${data.error || 'Delete failed'}` }] };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { content: [{ type: 'text', text: `File ${file_id} deleted successfully.` }] };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function handleBundleStatus(args) {
|
|
281
|
+
const { short_code } = args;
|
|
282
|
+
const response = await fetch(`${API}/bundle/${encodeURIComponent(short_code)}/status`);
|
|
283
|
+
const data = await response.json();
|
|
284
|
+
|
|
285
|
+
if (!data.success) {
|
|
286
|
+
return { content: [{ type: 'text', text: `Error: ${data.error || 'Bundle not found'}` }] };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const lines = [
|
|
290
|
+
`Bundle: ${short_code}`,
|
|
291
|
+
`Exists: ${data.exists ? 'Yes' : 'No'}`,
|
|
292
|
+
`Expired: ${data.is_expired ? 'Yes' : 'No'}`,
|
|
293
|
+
`Expires: ${data.expires_at}`,
|
|
294
|
+
`Used: ${formatSize(data.total_size_bytes)} / ${formatSize(data.max_size_bytes)}`,
|
|
295
|
+
`Remaining: ${formatSize(data.space_remaining)}`,
|
|
296
|
+
`Views: ${data.view_count}`,
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function formatSize(bytes) {
|
|
303
|
+
if (bytes >= 1073741824) return (bytes / 1073741824).toFixed(2) + ' GB';
|
|
304
|
+
if (bytes >= 1048576) return (bytes / 1048576).toFixed(2) + ' MB';
|
|
305
|
+
if (bytes >= 1024) return (bytes / 1024).toFixed(2) + ' KB';
|
|
306
|
+
return bytes + ' B';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// --- MCP Server ---
|
|
310
|
+
|
|
311
|
+
const server = new Server(
|
|
312
|
+
{ name: 'easysend', version: '1.0.0' },
|
|
313
|
+
{ capabilities: { tools: {} } }
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
317
|
+
tools: TOOLS,
|
|
318
|
+
}));
|
|
319
|
+
|
|
320
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
321
|
+
const { name, arguments: args } = request.params;
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
switch (name) {
|
|
325
|
+
case 'upload_files': return await handleUpload(args);
|
|
326
|
+
case 'get_bundle': return await handleGetBundle(args);
|
|
327
|
+
case 'download_file': return await handleDownloadFile(args);
|
|
328
|
+
case 'download_bundle': return await handleDownloadBundle(args);
|
|
329
|
+
case 'delete_file': return await handleDeleteFile(args);
|
|
330
|
+
case 'bundle_status': return await handleBundleStatus(args);
|
|
331
|
+
default:
|
|
332
|
+
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
|
|
333
|
+
}
|
|
334
|
+
} catch (error) {
|
|
335
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const transport = new StdioServerTransport();
|
|
340
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "easysend-mcp",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"mcpName": "io.github.Easysend-co/easysend",
|
|
5
|
+
"description": "EasySend file sharing MCP server - upload, download and share files. No authentication required.",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"easysend-mcp": "./index.js"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
13
|
+
},
|
|
14
|
+
"keywords": ["mcp", "claude-code", "file-sharing", "easysend", "upload", "model-context-protocol"],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/Easysend-co/easysend-mcp.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://easysend.co/mcp",
|
|
21
|
+
"author": "EasySend <admin@easysend.co>"
|
|
22
|
+
}
|