uploadFile
Upload files to IPFS storage via Pinata MCP server using file paths or encoded content, supporting both public and private networks with metadata options.
Instructions
Upload a file to Pinata IPFS. Provide either a file:// URI or base64-encoded content.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| resourceUri | No | The file:// URI of the file to upload (e.g., file:///path/to/file.jpg) | |
| fileContent | No | Base64-encoded file content (use this if not providing resourceUri) | |
| fileName | No | Name for the uploaded file (auto-detected from path if using resourceUri) | |
| mimeType | No | MIME type of the file (auto-detected if not provided) | |
| network | No | Whether to upload to public or private IPFS | public |
| group_id | No | ID of a group to add the file to | |
| keyvalues | No | Metadata key-value pairs for the file |
Implementation Reference
- src/index.ts:333-444 (handler)The complete uploadFile tool implementation including registration, schema definition, and the async handler function that processes file uploads to Pinata IPFS. Supports both file:// URI and base64-encoded content modes.
server.tool( "uploadFile", "Upload a file to Pinata IPFS. Provide either a file:// URI or base64-encoded content.", { resourceUri: z .string() .optional() .describe("The file:// URI of the file to upload (e.g., file:///path/to/file.jpg)"), fileContent: z .string() .optional() .describe("Base64-encoded file content (use this if not providing resourceUri)"), fileName: z .string() .optional() .describe("Name for the uploaded file (auto-detected from path if using resourceUri)"), mimeType: z .string() .optional() .describe("MIME type of the file (auto-detected if not provided)"), network: z .enum(["public", "private"]) .default("public") .describe("Whether to upload to public or private IPFS"), group_id: z .string() .optional() .describe("ID of a group to add the file to"), keyvalues: z .record(z.string()) .optional() .describe("Metadata key-value pairs for the file"), }, async ({ resourceUri, fileContent, fileName, mimeType, network, group_id, keyvalues }) => { try { let fileBuffer: Buffer; let finalFileName: string; if (resourceUri) { // File path mode if (!resourceUri.startsWith("file://")) { throw new Error("resourceUri must be a file:// URI"); } let filePath: string; if (process.platform === "win32") { filePath = decodeURIComponent( resourceUri.replace(/^file:\/\/\//, "").replace(/\//g, "\\") ); } else { filePath = decodeURIComponent(resourceUri.replace(/^file:\/\//, "")); } // Validate path is allowed filePath = await validatePath(filePath); fileBuffer = await fs.readFile(filePath); finalFileName = fileName || path.basename(filePath); } else if (fileContent) { // Base64 content mode if (!fileName) { throw new Error("fileName is required when using fileContent"); } fileBuffer = Buffer.from(fileContent, "base64"); finalFileName = fileName; } else { throw new Error("Either resourceUri or fileContent must be provided"); } const detectedMimeType = mimeType || getMimeType(finalFileName); const formData = new FormData(); const blob = new Blob([new Uint8Array(fileBuffer)], { type: detectedMimeType }); formData.append("file", blob, finalFileName); formData.append("network", network); if (group_id) { formData.append("group_id", group_id); } if (keyvalues) { formData.append("keyvalues", JSON.stringify(keyvalues)); } const response = await fetch("https://uploads.pinata.cloud/v3/files", { method: "POST", headers: { Authorization: `Bearer ${PINATA_JWT}`, }, body: formData, }); if (!response.ok) { const errorText = await response.text(); throw new Error( `Failed to upload file: ${response.status} ${response.statusText}\n${errorText}` ); } const data = await response.json(); return { content: [ { type: "text", text: `✅ File uploaded successfully!\n\n${JSON.stringify(data, null, 2)}`, }, ], }; } catch (error) { return errorResponse(error); } } ); - src/index.ts:336-365 (schema)Input parameter schema for uploadFile tool using Zod validation. Defines parameters: resourceUri, fileContent, fileName, mimeType, network, group_id, and keyvalues.
{ resourceUri: z .string() .optional() .describe("The file:// URI of the file to upload (e.g., file:///path/to/file.jpg)"), fileContent: z .string() .optional() .describe("Base64-encoded file content (use this if not providing resourceUri)"), fileName: z .string() .optional() .describe("Name for the uploaded file (auto-detected from path if using resourceUri)"), mimeType: z .string() .optional() .describe("MIME type of the file (auto-detected if not provided)"), network: z .enum(["public", "private"]) .default("public") .describe("Whether to upload to public or private IPFS"), group_id: z .string() .optional() .describe("ID of a group to add the file to"), keyvalues: z .record(z.string()) .optional() .describe("Metadata key-value pairs for the file"), }, - src/index.ts:366-443 (handler)The core uploadFile handler logic that validates input, reads file content (either from path or base64), constructs multipart form data, and sends POST request to Pinata's upload endpoint.
async ({ resourceUri, fileContent, fileName, mimeType, network, group_id, keyvalues }) => { try { let fileBuffer: Buffer; let finalFileName: string; if (resourceUri) { // File path mode if (!resourceUri.startsWith("file://")) { throw new Error("resourceUri must be a file:// URI"); } let filePath: string; if (process.platform === "win32") { filePath = decodeURIComponent( resourceUri.replace(/^file:\/\/\//, "").replace(/\//g, "\\") ); } else { filePath = decodeURIComponent(resourceUri.replace(/^file:\/\//, "")); } // Validate path is allowed filePath = await validatePath(filePath); fileBuffer = await fs.readFile(filePath); finalFileName = fileName || path.basename(filePath); } else if (fileContent) { // Base64 content mode if (!fileName) { throw new Error("fileName is required when using fileContent"); } fileBuffer = Buffer.from(fileContent, "base64"); finalFileName = fileName; } else { throw new Error("Either resourceUri or fileContent must be provided"); } const detectedMimeType = mimeType || getMimeType(finalFileName); const formData = new FormData(); const blob = new Blob([new Uint8Array(fileBuffer)], { type: detectedMimeType }); formData.append("file", blob, finalFileName); formData.append("network", network); if (group_id) { formData.append("group_id", group_id); } if (keyvalues) { formData.append("keyvalues", JSON.stringify(keyvalues)); } const response = await fetch("https://uploads.pinata.cloud/v3/files", { method: "POST", headers: { Authorization: `Bearer ${PINATA_JWT}`, }, body: formData, }); if (!response.ok) { const errorText = await response.text(); throw new Error( `Failed to upload file: ${response.status} ${response.statusText}\n${errorText}` ); } const data = await response.json(); return { content: [ { type: "text", text: `✅ File uploaded successfully!\n\n${JSON.stringify(data, null, 2)}`, }, ], }; } catch (error) { return errorResponse(error); } } - src/index.ts:36-89 (helper)validatePath helper function used by uploadFile to ensure file paths are within allowed directories for security. Resolves symlinks and validates access permissions.
async function validatePath(requestedPath: string): Promise<string> { // If no directories specified, allow current working directory const dirsToCheck = allowedDirectories.length > 0 ? allowedDirectories : [normalizePath(process.cwd())]; const expandedPath = expandHome(requestedPath); const absolute = path.isAbsolute(expandedPath) ? path.resolve(expandedPath) : path.resolve(process.cwd(), expandedPath); const normalizedRequested = normalizePath(absolute); const isAllowed = dirsToCheck.some((dir) => normalizedRequested.startsWith(dir) ); if (!isAllowed) { throw new Error( `Access denied - path outside allowed directories: ${absolute}` ); } try { const realPath = await fs.realpath(absolute); const normalizedReal = normalizePath(realPath); const isRealPathAllowed = dirsToCheck.some((dir) => normalizedReal.startsWith(dir) ); if (!isRealPathAllowed) { throw new Error( "Access denied - symlink target outside allowed directories" ); } return realPath; } catch (error) { const parentDir = path.dirname(absolute); try { const realParentPath = await fs.realpath(parentDir); const normalizedParent = normalizePath(realParentPath); const isParentAllowed = dirsToCheck.some((dir) => normalizedParent.startsWith(dir) ); if (!isParentAllowed) { throw new Error( "Access denied - parent directory outside allowed directories" ); } return absolute; } catch { throw new Error(`Parent directory does not exist: ${parentDir}`); } } } - src/index.ts:1821-1850 (helper)getMimeType helper function used by uploadFile to auto-detect MIME types based on file extensions when not explicitly provided.
function getMimeType(filePath: string): string { const extension = filePath.split(".").pop()?.toLowerCase() || ""; const mimeTypes: Record<string, string> = { txt: "text/plain", html: "text/html", css: "text/css", js: "application/javascript", json: "application/json", xml: "application/xml", pdf: "application/pdf", jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", svg: "image/svg+xml", webp: "image/webp", mp3: "audio/mpeg", mp4: "video/mp4", webm: "video/webm", zip: "application/zip", doc: "application/msword", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", xls: "application/vnd.ms-excel", xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ppt: "application/vnd.ms-powerpoint", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation", }; return mimeTypes[extension] || "application/octet-stream"; }