@objekt.sh/mcp-upload 0.1.2 → 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.
Files changed (2) hide show
  1. package/dist/index.js +155 -28
  2. package/package.json +1 -1
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: "0.1.0" },
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 dataURL;
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 bytes = await readFile(absPath);
123
- const mime = mimeFromPath(absPath);
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.length > MAX_CONTENT_BYTES) {
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
- const b64 = Buffer.from(content).toString("base64");
153
- dataURL = `data:${content_type};base64,${b64}`;
154
+ fileBytes = new Uint8Array(Buffer.from(content));
154
155
  } else {
155
- dataURL = `data:${content_type};base64,${content}`;
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
- "Content-Type": "application/json",
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 environment (e.g. Claude Desktop VM, claude.ai container). Returns a bash command to run in the sandbox shell. Use this when the file is at a sandbox path like /mnt/user-data/ that the host cannot access.",
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: true,
222
- openWorldHint: false
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 mime = mimeFromPath(sandbox_path);
239
- const cmd = `curl -s -X PUT "${GATEWAY_URL}/${fileName}" \\
240
- -H "Content-Type: application/json" \\
241
- -H "Authorization: Bearer ${API_KEY}" \\
242
- -d "{\\"dataURL\\":\\"data:${mime};base64,$(base64 -w0 "${sandbox_path}" 2>/dev/null || base64 -i "${sandbox_path}")\\"}"`;
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: `Run this command in your shell to upload the file:
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.2",
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": {