@objekt.sh/mcp-upload 0.1.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/LICENSE +17 -0
- package/dist/index.js +334 -0
- package/package.json +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 3, 19 November 2007
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2026 0xLighthouse
|
|
5
|
+
|
|
6
|
+
This program is free software: you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
8
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU Affero General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU Affero General Public License
|
|
17
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { readFile, stat } from "fs/promises";
|
|
5
|
+
import { basename, extname, resolve } from "path";
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
var GATEWAY_URL = process.env.OBJEKT_GATEWAY_URL ?? "https://api.objekt.sh";
|
|
10
|
+
var API_KEY = process.env.OBJEKT_API_KEY ?? "";
|
|
11
|
+
var MIME_MAP = {
|
|
12
|
+
".jpg": "image/jpeg",
|
|
13
|
+
".jpeg": "image/jpeg",
|
|
14
|
+
".png": "image/png",
|
|
15
|
+
".webp": "image/webp",
|
|
16
|
+
".gif": "image/gif",
|
|
17
|
+
".svg": "image/svg+xml",
|
|
18
|
+
".pdf": "application/pdf",
|
|
19
|
+
".html": "text/html",
|
|
20
|
+
".css": "text/css",
|
|
21
|
+
".js": "application/javascript",
|
|
22
|
+
".json": "application/json",
|
|
23
|
+
".txt": "text/plain",
|
|
24
|
+
".md": "text/markdown"
|
|
25
|
+
};
|
|
26
|
+
function mimeFromPath(filePath) {
|
|
27
|
+
return MIME_MAP[extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
28
|
+
}
|
|
29
|
+
var MAX_CONTENT_BYTES = 500 * 1024;
|
|
30
|
+
var server = new McpServer(
|
|
31
|
+
{ name: "Objekt.sh", version: "0.1.0" },
|
|
32
|
+
{
|
|
33
|
+
instructions: [
|
|
34
|
+
"Objekt.sh uploads files to decentralised storage (CDN, IPFS, Arweave).",
|
|
35
|
+
"",
|
|
36
|
+
"Encoding: use 'raw' for text formats (SVG, HTML, CSS, JSON, Markdown) \u2014 no base64 overhead.",
|
|
37
|
+
"Use 'base64' (default) for binary formats (PNG, JPEG, WebP, GIF, PDF).",
|
|
38
|
+
"",
|
|
39
|
+
"Supported formats: JPEG, PNG, WebP, GIF, SVG, PDF (up to 5MB via path, 500KB via content).",
|
|
40
|
+
"For host filesystem files, ALWAYS prefer the 'path' parameter \u2014 reads from disk, zero token cost.",
|
|
41
|
+
"Inline 'content' is capped at 500KB to avoid wasting tokens. Use 'path' for anything larger.",
|
|
42
|
+
"",
|
|
43
|
+
"Uploaded files return a permalink. Use get_file to check if a file already exists before re-uploading."
|
|
44
|
+
].join("\n")
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
server.registerTool(
|
|
48
|
+
"upload_file",
|
|
49
|
+
{
|
|
50
|
+
title: "Upload File",
|
|
51
|
+
description: `Upload a file to objekt.sh storage. Accepts a host file path OR base64/raw content.
|
|
52
|
+
|
|
53
|
+
For files on the HOST filesystem (e.g. /Users/name/photo.png): use 'path' \u2014 reads from disk, fastest.
|
|
54
|
+
For files you have in memory: use 'content' + 'content_type'.
|
|
55
|
+
For files at CONTAINER paths (e.g. /mnt/user-data/): use the upload_from_sandbox tool instead.`,
|
|
56
|
+
inputSchema: z.object({
|
|
57
|
+
path: z.string().optional().describe(
|
|
58
|
+
"Absolute path to the file on the HOST filesystem (e.g. /Users/you/photo.png). Do NOT use container paths like /mnt/user-data/."
|
|
59
|
+
),
|
|
60
|
+
content: z.string().optional().describe(
|
|
61
|
+
"File content \u2014 base64-encoded for binaries, raw UTF-8 for text. Use this when the file path is not accessible from the host filesystem."
|
|
62
|
+
),
|
|
63
|
+
content_type: z.string().optional().describe(
|
|
64
|
+
"MIME type (required with content, e.g. 'image/png'). Auto-detected when using path."
|
|
65
|
+
),
|
|
66
|
+
encoding: z.enum(["base64", "raw"]).optional().describe(
|
|
67
|
+
"Content encoding. Default 'base64'. Use 'raw' for text types like SVG, HTML, CSS, JSON."
|
|
68
|
+
),
|
|
69
|
+
name: z.string().optional().describe(
|
|
70
|
+
"Filename for the upload. Auto-detected from 'path'. Required when using 'content'."
|
|
71
|
+
)
|
|
72
|
+
}),
|
|
73
|
+
annotations: {
|
|
74
|
+
title: "Upload File",
|
|
75
|
+
readOnlyHint: false,
|
|
76
|
+
openWorldHint: true
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
async ({
|
|
80
|
+
path: filePath,
|
|
81
|
+
content,
|
|
82
|
+
content_type,
|
|
83
|
+
encoding = "base64",
|
|
84
|
+
name: customName
|
|
85
|
+
}) => {
|
|
86
|
+
if (!API_KEY) {
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: "text",
|
|
91
|
+
text: "OBJEKT_API_KEY not set. Get a free key at objekt.sh/mcp"
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
isError: true
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
let dataURL;
|
|
98
|
+
let fileName;
|
|
99
|
+
if (filePath) {
|
|
100
|
+
if (filePath.startsWith("/mnt/") || filePath.startsWith("/sandbox/") || filePath.startsWith("/tmp/sandbox")) {
|
|
101
|
+
return {
|
|
102
|
+
content: [
|
|
103
|
+
{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: `"${filePath}" is a sandbox path \u2014 this tool runs on the host and cannot access it. Use upload_from_sandbox for sandbox files, or pass the content directly via the "content" parameter with "encoding": "raw" for text files or "base64" for binaries.`
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
isError: true
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const absPath = resolve(filePath);
|
|
112
|
+
try {
|
|
113
|
+
await stat(absPath);
|
|
114
|
+
} catch {
|
|
115
|
+
return {
|
|
116
|
+
content: [
|
|
117
|
+
{ type: "text", text: `File not found: ${absPath}` }
|
|
118
|
+
],
|
|
119
|
+
isError: true
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const bytes = await readFile(absPath);
|
|
123
|
+
const mime = mimeFromPath(absPath);
|
|
124
|
+
fileName = customName ?? basename(absPath);
|
|
125
|
+
const b64 = bytes.toString("base64");
|
|
126
|
+
dataURL = `data:${mime};base64,${b64}`;
|
|
127
|
+
} else if (content && content_type) {
|
|
128
|
+
if (content.length > MAX_CONTENT_BYTES) {
|
|
129
|
+
return {
|
|
130
|
+
content: [
|
|
131
|
+
{
|
|
132
|
+
type: "text",
|
|
133
|
+
text: `Content too large (${(content.length / 1024).toFixed(0)}KB). Inline content is capped at ${MAX_CONTENT_BYTES / 1024}KB to avoid burning tokens. Use the 'path' parameter instead \u2014 it reads from disk with zero token overhead.`
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
isError: true
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (!customName) {
|
|
140
|
+
return {
|
|
141
|
+
content: [
|
|
142
|
+
{
|
|
143
|
+
type: "text",
|
|
144
|
+
text: "'name' is required when using 'content' mode (e.g. 'diagram.svg'). Auto-detected only with 'path'."
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
isError: true
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
fileName = customName;
|
|
151
|
+
if (encoding === "raw") {
|
|
152
|
+
const b64 = Buffer.from(content).toString("base64");
|
|
153
|
+
dataURL = `data:${content_type};base64,${b64}`;
|
|
154
|
+
} else {
|
|
155
|
+
dataURL = `data:${content_type};base64,${content}`;
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
return {
|
|
159
|
+
content: [
|
|
160
|
+
{
|
|
161
|
+
type: "text",
|
|
162
|
+
text: "Provide either 'path' (file path on host) or 'content' + 'content_type' (base64/raw content)."
|
|
163
|
+
}
|
|
164
|
+
],
|
|
165
|
+
isError: true
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const res = await fetch(`${GATEWAY_URL}/${fileName}`, {
|
|
169
|
+
method: "PUT",
|
|
170
|
+
headers: {
|
|
171
|
+
"Content-Type": "application/json",
|
|
172
|
+
Authorization: `Bearer ${API_KEY}`
|
|
173
|
+
},
|
|
174
|
+
body: JSON.stringify({ dataURL })
|
|
175
|
+
});
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
const text = await res.text();
|
|
178
|
+
return {
|
|
179
|
+
content: [
|
|
180
|
+
{
|
|
181
|
+
type: "text",
|
|
182
|
+
text: `Upload failed (${res.status}): ${text}`
|
|
183
|
+
}
|
|
184
|
+
],
|
|
185
|
+
isError: true
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const data = await res.json();
|
|
189
|
+
return {
|
|
190
|
+
content: [
|
|
191
|
+
{
|
|
192
|
+
type: "resource_link",
|
|
193
|
+
uri: data.permalink,
|
|
194
|
+
name: data.name,
|
|
195
|
+
mimeType: data.kind
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
type: "text",
|
|
199
|
+
text: `Uploaded ${data.name} (${data.kind}, ${data.bytes} bytes)
|
|
200
|
+
${data.permalink}`
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
server.registerTool(
|
|
207
|
+
"upload_from_sandbox",
|
|
208
|
+
{
|
|
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.",
|
|
211
|
+
inputSchema: z.object({
|
|
212
|
+
sandbox_path: z.string().describe(
|
|
213
|
+
"Path to the file inside the sandbox (e.g. /mnt/user-data/uploads/photo.png)"
|
|
214
|
+
),
|
|
215
|
+
name: z.string().optional().describe(
|
|
216
|
+
"Custom name for the uploaded file. Defaults to the filename."
|
|
217
|
+
)
|
|
218
|
+
}),
|
|
219
|
+
annotations: {
|
|
220
|
+
title: "Upload from Sandbox",
|
|
221
|
+
readOnlyHint: true,
|
|
222
|
+
openWorldHint: false
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
async ({ sandbox_path, name: customName }) => {
|
|
226
|
+
if (!API_KEY) {
|
|
227
|
+
return {
|
|
228
|
+
content: [
|
|
229
|
+
{
|
|
230
|
+
type: "text",
|
|
231
|
+
text: "OBJEKT_API_KEY not set. Get a free key at objekt.sh/mcp"
|
|
232
|
+
}
|
|
233
|
+
],
|
|
234
|
+
isError: true
|
|
235
|
+
};
|
|
236
|
+
}
|
|
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}")\\"}"`;
|
|
243
|
+
return {
|
|
244
|
+
content: [
|
|
245
|
+
{
|
|
246
|
+
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.`
|
|
252
|
+
}
|
|
253
|
+
]
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
server.registerTool(
|
|
258
|
+
"get_file",
|
|
259
|
+
{
|
|
260
|
+
title: "Get File",
|
|
261
|
+
description: "Get a file from objekt.sh by name. Returns a link to the file without downloading the content.",
|
|
262
|
+
inputSchema: z.object({
|
|
263
|
+
name: z.string().describe("File name/key (e.g. 'photo.png')")
|
|
264
|
+
}),
|
|
265
|
+
annotations: {
|
|
266
|
+
title: "Get File",
|
|
267
|
+
readOnlyHint: true,
|
|
268
|
+
openWorldHint: false
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
async ({ name }) => {
|
|
272
|
+
const res = await fetch(`${GATEWAY_URL}/${name}`, {
|
|
273
|
+
method: "HEAD",
|
|
274
|
+
headers: API_KEY ? { Authorization: `Bearer ${API_KEY}` } : {}
|
|
275
|
+
});
|
|
276
|
+
if (!res.ok) {
|
|
277
|
+
return {
|
|
278
|
+
content: [
|
|
279
|
+
{
|
|
280
|
+
type: "text",
|
|
281
|
+
text: `File not found: ${name} (${res.status})`
|
|
282
|
+
}
|
|
283
|
+
],
|
|
284
|
+
isError: true
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const contentType = res.headers.get("content-type") ?? "application/octet-stream";
|
|
288
|
+
const size = res.headers.get("content-length") ?? "unknown";
|
|
289
|
+
const url = `${GATEWAY_URL}/${name}`;
|
|
290
|
+
return {
|
|
291
|
+
content: [
|
|
292
|
+
{
|
|
293
|
+
type: "resource_link",
|
|
294
|
+
uri: url,
|
|
295
|
+
name,
|
|
296
|
+
mimeType: contentType
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
type: "text",
|
|
300
|
+
text: `${name} \u2014 ${contentType}, ${size} bytes
|
|
301
|
+
${url}`
|
|
302
|
+
}
|
|
303
|
+
]
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
);
|
|
307
|
+
server.registerTool(
|
|
308
|
+
"get_pricing",
|
|
309
|
+
{
|
|
310
|
+
title: "Get Pricing",
|
|
311
|
+
description: "Get current storage pricing and limits from objekt.sh.",
|
|
312
|
+
inputSchema: z.object({}),
|
|
313
|
+
annotations: {
|
|
314
|
+
title: "Get Pricing",
|
|
315
|
+
readOnlyHint: true,
|
|
316
|
+
openWorldHint: false
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
async () => {
|
|
320
|
+
const res = await fetch(`${GATEWAY_URL}/pricing`);
|
|
321
|
+
if (!res.ok) {
|
|
322
|
+
return {
|
|
323
|
+
content: [{ type: "text", text: "Failed to fetch pricing" }],
|
|
324
|
+
isError: true
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const data = await res.json();
|
|
328
|
+
return {
|
|
329
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
);
|
|
333
|
+
var transport = new StdioServerTransport();
|
|
334
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@objekt.sh/mcp-upload",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"mcpName": "sh.objekt/mcp-upload",
|
|
5
|
+
"description": "MCP server for uploading files to objekt.sh from Claude Desktop, Cursor, and other MCP clients.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/0xLighthouse/objekt-mcp-upload.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"bin": {
|
|
12
|
+
"mcp-upload": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
19
|
+
"zod": "^3.25.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"tsup": "^8.5.1",
|
|
23
|
+
"tsx": "^4.21.0",
|
|
24
|
+
"typescript": "^5.8.2"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup src/index.ts --format esm",
|
|
31
|
+
"dev": "tsx src/index.ts",
|
|
32
|
+
"lint": "biome check"
|
|
33
|
+
}
|
|
34
|
+
}
|