@objekt.sh/mcp-upload 0.1.1 → 0.1.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/README.md +34 -0
- package/dist/index.js +155 -28
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# @objekt.sh/mcp-upload
|
|
2
|
+
|
|
3
|
+
MCP server for uploading files to decentralised storage from Claude Desktop, Cursor, or any MCP client.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Get a free API key at [objekt.sh/mcp](https://objekt.sh/mcp), then add to your MCP config:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"objekt": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["-y", "@objekt.sh/mcp-upload"],
|
|
15
|
+
"env": {
|
|
16
|
+
"OBJEKT_API_KEY": "objekt_mcp_..."
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Tools
|
|
24
|
+
|
|
25
|
+
| Tool | Description |
|
|
26
|
+
|------|-------------|
|
|
27
|
+
| `upload_file` | Upload by host path or inline content |
|
|
28
|
+
| `upload_from_sandbox` | Returns curl command for sandbox uploads |
|
|
29
|
+
| `get_file` | Get file metadata and permalink |
|
|
30
|
+
| `get_pricing` | Current storage tier pricing |
|
|
31
|
+
|
|
32
|
+
## Docs
|
|
33
|
+
|
|
34
|
+
[objekt.sh/mcp](https://objekt.sh/mcp) — setup, pricing, best practices.
|
package/dist/index.js
CHANGED
|
@@ -27,8 +27,9 @@ function mimeFromPath(filePath) {
|
|
|
27
27
|
return MIME_MAP[extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
28
28
|
}
|
|
29
29
|
var MAX_CONTENT_BYTES = 500 * 1024;
|
|
30
|
+
var VERSION = "0.1.5";
|
|
30
31
|
var server = new McpServer(
|
|
31
|
-
{ name: "Objekt.sh", version:
|
|
32
|
+
{ name: "Objekt.sh", version: VERSION },
|
|
32
33
|
{
|
|
33
34
|
instructions: [
|
|
34
35
|
"Objekt.sh uploads files to decentralised storage (CDN, IPFS, Arweave).",
|
|
@@ -94,7 +95,8 @@ For files at CONTAINER paths (e.g. /mnt/user-data/): use the upload_from_sandbox
|
|
|
94
95
|
isError: true
|
|
95
96
|
};
|
|
96
97
|
}
|
|
97
|
-
let
|
|
98
|
+
let fileBytes;
|
|
99
|
+
let fileMime;
|
|
98
100
|
let fileName;
|
|
99
101
|
if (filePath) {
|
|
100
102
|
if (filePath.startsWith("/mnt/") || filePath.startsWith("/sandbox/") || filePath.startsWith("/tmp/sandbox")) {
|
|
@@ -119,13 +121,12 @@ For files at CONTAINER paths (e.g. /mnt/user-data/): use the upload_from_sandbox
|
|
|
119
121
|
isError: true
|
|
120
122
|
};
|
|
121
123
|
}
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
+
const buffer = await readFile(absPath);
|
|
125
|
+
fileBytes = new Uint8Array(buffer);
|
|
126
|
+
fileMime = mimeFromPath(absPath);
|
|
124
127
|
fileName = customName ?? basename(absPath);
|
|
125
|
-
const b64 = bytes.toString("base64");
|
|
126
|
-
dataURL = `data:${mime};base64,${b64}`;
|
|
127
128
|
} else if (content && content_type) {
|
|
128
|
-
if (content
|
|
129
|
+
if (Buffer.byteLength(content) > MAX_CONTENT_BYTES) {
|
|
129
130
|
return {
|
|
130
131
|
content: [
|
|
131
132
|
{
|
|
@@ -148,11 +149,11 @@ For files at CONTAINER paths (e.g. /mnt/user-data/): use the upload_from_sandbox
|
|
|
148
149
|
};
|
|
149
150
|
}
|
|
150
151
|
fileName = customName;
|
|
152
|
+
fileMime = content_type;
|
|
151
153
|
if (encoding === "raw") {
|
|
152
|
-
|
|
153
|
-
dataURL = `data:${content_type};base64,${b64}`;
|
|
154
|
+
fileBytes = new Uint8Array(Buffer.from(content));
|
|
154
155
|
} else {
|
|
155
|
-
|
|
156
|
+
fileBytes = new Uint8Array(Buffer.from(content, "base64"));
|
|
156
157
|
}
|
|
157
158
|
} else {
|
|
158
159
|
return {
|
|
@@ -165,13 +166,12 @@ For files at CONTAINER paths (e.g. /mnt/user-data/): use the upload_from_sandbox
|
|
|
165
166
|
isError: true
|
|
166
167
|
};
|
|
167
168
|
}
|
|
169
|
+
const form = new FormData();
|
|
170
|
+
form.append("file", new Blob([fileBytes], { type: fileMime }), fileName);
|
|
168
171
|
const res = await fetch(`${GATEWAY_URL}/${fileName}`, {
|
|
169
172
|
method: "PUT",
|
|
170
|
-
headers: {
|
|
171
|
-
|
|
172
|
-
Authorization: `Bearer ${API_KEY}`
|
|
173
|
-
},
|
|
174
|
-
body: JSON.stringify({ dataURL })
|
|
173
|
+
headers: { Authorization: `Bearer ${API_KEY}` },
|
|
174
|
+
body: form
|
|
175
175
|
});
|
|
176
176
|
if (!res.ok) {
|
|
177
177
|
const text = await res.text();
|
|
@@ -207,7 +207,7 @@ server.registerTool(
|
|
|
207
207
|
"upload_from_sandbox",
|
|
208
208
|
{
|
|
209
209
|
title: "Upload from Sandbox",
|
|
210
|
-
description: "Upload a file from a sandbox
|
|
210
|
+
description: "Upload a file from a sandbox/container path (e.g. /mnt/user-data/, /home/claude/) that the host cannot access. Reads the file and uploads it directly \u2014 single tool call, no shell commands needed.",
|
|
211
211
|
inputSchema: z.object({
|
|
212
212
|
sandbox_path: z.string().describe(
|
|
213
213
|
"Path to the file inside the sandbox (e.g. /mnt/user-data/uploads/photo.png)"
|
|
@@ -218,8 +218,8 @@ server.registerTool(
|
|
|
218
218
|
}),
|
|
219
219
|
annotations: {
|
|
220
220
|
title: "Upload from Sandbox",
|
|
221
|
-
readOnlyHint:
|
|
222
|
-
openWorldHint:
|
|
221
|
+
readOnlyHint: false,
|
|
222
|
+
openWorldHint: true
|
|
223
223
|
}
|
|
224
224
|
},
|
|
225
225
|
async ({ sandbox_path, name: customName }) => {
|
|
@@ -235,20 +235,54 @@ server.registerTool(
|
|
|
235
235
|
};
|
|
236
236
|
}
|
|
237
237
|
const fileName = customName ?? basename(sandbox_path);
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
238
|
+
const fileMime = mimeFromPath(sandbox_path);
|
|
239
|
+
let fileBytes;
|
|
240
|
+
try {
|
|
241
|
+
const buffer = await readFile(sandbox_path);
|
|
242
|
+
fileBytes = new Uint8Array(buffer);
|
|
243
|
+
} catch {
|
|
244
|
+
return {
|
|
245
|
+
content: [
|
|
246
|
+
{
|
|
247
|
+
type: "text",
|
|
248
|
+
text: `File not found or not readable: ${sandbox_path}`
|
|
249
|
+
}
|
|
250
|
+
],
|
|
251
|
+
isError: true
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const form = new FormData();
|
|
255
|
+
form.append("file", new Blob([fileBytes], { type: fileMime }), fileName);
|
|
256
|
+
const res = await fetch(`${GATEWAY_URL}/${fileName}`, {
|
|
257
|
+
method: "PUT",
|
|
258
|
+
headers: { Authorization: `Bearer ${API_KEY}` },
|
|
259
|
+
body: form
|
|
260
|
+
});
|
|
261
|
+
if (!res.ok) {
|
|
262
|
+
const text = await res.text();
|
|
263
|
+
return {
|
|
264
|
+
content: [
|
|
265
|
+
{
|
|
266
|
+
type: "text",
|
|
267
|
+
text: `Upload failed (${res.status}): ${text}`
|
|
268
|
+
}
|
|
269
|
+
],
|
|
270
|
+
isError: true
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
const data = await res.json();
|
|
243
274
|
return {
|
|
244
275
|
content: [
|
|
276
|
+
{
|
|
277
|
+
type: "resource_link",
|
|
278
|
+
uri: data.permalink,
|
|
279
|
+
name: data.name,
|
|
280
|
+
mimeType: data.kind
|
|
281
|
+
},
|
|
245
282
|
{
|
|
246
283
|
type: "text",
|
|
247
|
-
text: `
|
|
248
|
-
|
|
249
|
-
${cmd}
|
|
250
|
-
|
|
251
|
-
This reads the file from the container filesystem and uploads it directly to objekt.sh.`
|
|
284
|
+
text: `Uploaded ${data.name} (${data.kind}, ${data.bytes} bytes)
|
|
285
|
+
${data.permalink}`
|
|
252
286
|
}
|
|
253
287
|
]
|
|
254
288
|
};
|
|
@@ -330,5 +364,98 @@ server.registerTool(
|
|
|
330
364
|
};
|
|
331
365
|
}
|
|
332
366
|
);
|
|
367
|
+
server.registerTool(
|
|
368
|
+
"check_connection",
|
|
369
|
+
{
|
|
370
|
+
title: "Check Connection",
|
|
371
|
+
description: "Test connectivity to api.objekt.sh. Use this to diagnose sandbox egress issues. If it fails, the user needs to add api.objekt.sh to their domain allowlist (Settings \u2192 Capabilities \u2192 Domain Allowlist).",
|
|
372
|
+
inputSchema: z.object({}),
|
|
373
|
+
annotations: {
|
|
374
|
+
title: "Check Connection",
|
|
375
|
+
readOnlyHint: true,
|
|
376
|
+
openWorldHint: true
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
async () => {
|
|
380
|
+
const checks = {
|
|
381
|
+
gateway: GATEWAY_URL,
|
|
382
|
+
apiKey: API_KEY ? "set" : "missing",
|
|
383
|
+
egress: "unknown",
|
|
384
|
+
latencyMs: 0
|
|
385
|
+
};
|
|
386
|
+
const start = Date.now();
|
|
387
|
+
try {
|
|
388
|
+
const res = await fetch(`${GATEWAY_URL}/pricing`, {
|
|
389
|
+
signal: AbortSignal.timeout(1e4)
|
|
390
|
+
});
|
|
391
|
+
checks.latencyMs = Date.now() - start;
|
|
392
|
+
checks.egress = res.ok ? "ok" : `http_${res.status}`;
|
|
393
|
+
} catch {
|
|
394
|
+
checks.latencyMs = Date.now() - start;
|
|
395
|
+
checks.egress = "blocked";
|
|
396
|
+
}
|
|
397
|
+
if (checks.egress === "blocked") {
|
|
398
|
+
return {
|
|
399
|
+
content: [
|
|
400
|
+
{
|
|
401
|
+
type: "text",
|
|
402
|
+
text: `Cannot reach ${GATEWAY_URL}
|
|
403
|
+
|
|
404
|
+
This usually means network egress is blocked. To fix:
|
|
405
|
+
1. Go to Settings \u2192 Capabilities
|
|
406
|
+
2. Enable "Allow network egress"
|
|
407
|
+
3. Under Domain Allowlist, add: api.objekt.sh
|
|
408
|
+
|
|
409
|
+
API key: ${checks.apiKey}`
|
|
410
|
+
}
|
|
411
|
+
],
|
|
412
|
+
isError: true
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (checks.apiKey === "missing") {
|
|
416
|
+
return {
|
|
417
|
+
content: [
|
|
418
|
+
{
|
|
419
|
+
type: "text",
|
|
420
|
+
text: `Egress: ${checks.egress} (${checks.latencyMs}ms)
|
|
421
|
+
API key: missing \u2014 set OBJEKT_API_KEY in your MCP server config. Get a key at objekt.sh/mcp`
|
|
422
|
+
}
|
|
423
|
+
],
|
|
424
|
+
isError: true
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
content: [
|
|
429
|
+
{
|
|
430
|
+
type: "text",
|
|
431
|
+
text: `Egress: ${checks.egress} (${checks.latencyMs}ms)
|
|
432
|
+
Gateway: ${checks.gateway}
|
|
433
|
+
API key: ${checks.apiKey}`
|
|
434
|
+
}
|
|
435
|
+
]
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
);
|
|
439
|
+
server.registerTool(
|
|
440
|
+
"get_version",
|
|
441
|
+
{
|
|
442
|
+
title: "Get Version",
|
|
443
|
+
description: "Returns the installed version of the Objekt.sh MCP server.",
|
|
444
|
+
inputSchema: z.object({}),
|
|
445
|
+
annotations: {
|
|
446
|
+
title: "Get Version",
|
|
447
|
+
readOnlyHint: true,
|
|
448
|
+
openWorldHint: false
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
async () => ({
|
|
452
|
+
content: [
|
|
453
|
+
{
|
|
454
|
+
type: "text",
|
|
455
|
+
text: `objekt.sh/mcp-upload v${VERSION}`
|
|
456
|
+
}
|
|
457
|
+
]
|
|
458
|
+
})
|
|
459
|
+
);
|
|
333
460
|
var transport = new StdioServerTransport();
|
|
334
461
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objekt.sh/mcp-upload",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"mcpName": "sh.objekt/mcp-upload",
|
|
5
5
|
"description": "MCP server for uploading files to objekt.sh from Claude Desktop, Cursor, and other MCP clients.",
|
|
6
6
|
"repository": {
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"mcp-upload": "./dist/index.js"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
|
-
"dist"
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
16
17
|
],
|
|
17
18
|
"dependencies": {
|
|
18
19
|
"@modelcontextprotocol/sdk": "^1.29.0",
|