Skip to main content
Glama

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
NameRequiredDescriptionDefault
resourceUriNoThe file:// URI of the file to upload (e.g., file:///path/to/file.jpg)
fileContentNoBase64-encoded file content (use this if not providing resourceUri)
fileNameNoName for the uploaded file (auto-detected from path if using resourceUri)
mimeTypeNoMIME type of the file (auto-detected if not provided)
networkNoWhether to upload to public or private IPFSpublic
group_idNoID of a group to add the file to
keyvaluesNoMetadata key-value pairs for the file

Implementation Reference

  • 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);
        }
      }
    );
  • 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"),
    },
  • 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);
      }
    }
  • 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}`);
        }
      }
    }
  • 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";
    }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/PinataCloud/pinata-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server