Pinata MCP

Official
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import fs from "fs/promises"; import path from "path"; import os from 'os'; import { ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js"; // Command line argument parsing const args = process.argv.slice(2); if (args.length === 0) { console.error("Usage: pinata-mcp-server <allowed-directory> [additional-directories...]"); process.exit(1); } // Normalize all paths consistently function normalizePath(p: string): string { return path.normalize(p); } function expandHome(filepath: string): string { if (filepath.startsWith('~/') || filepath === '~') { return path.join(os.homedir(), filepath.slice(1)); } return filepath; } // Store allowed directories in normalized form const allowedDirectories = args.map(dir => normalizePath(path.resolve(expandHome(dir))) ); // Validate that all directories exist and are accessible (async () => { await Promise.all(args.map(async (dir) => { try { const expandedDir = expandHome(dir); const stats = await fs.stat(expandedDir); if (!stats.isDirectory()) { console.error(`Error: ${dir} is not a directory`); process.exit(1); } } catch (error) { console.error(`Error accessing directory ${dir}:`, error); process.exit(1); } })); })(); // Security utilities async function validatePath(requestedPath: string): Promise<string> { const expandedPath = expandHome(requestedPath); const absolute = path.isAbsolute(expandedPath) ? path.resolve(expandedPath) : path.resolve(process.cwd(), expandedPath); const normalizedRequested = normalizePath(absolute); // Check if path is within allowed directories const isAllowed = allowedDirectories.some(dir => normalizedRequested.startsWith(dir)); if (!isAllowed) { throw new Error(`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(', ')}`); } // Handle symlinks by checking their real path try { const realPath = await fs.realpath(absolute); const normalizedReal = normalizePath(realPath); const isRealPathAllowed = allowedDirectories.some(dir => normalizedReal.startsWith(dir)); if (!isRealPathAllowed) { throw new Error("Access denied - symlink target outside allowed directories"); } return realPath; } catch (error) { // For new files that don't exist yet, verify parent directory const parentDir = path.dirname(absolute); try { const realParentPath = await fs.realpath(parentDir); const normalizedParent = normalizePath(realParentPath); const isParentAllowed = allowedDirectories.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}`); } } } const server = new McpServer({ name: "Pinata", version: "1.0.0", capabilities: { tools: {}, resources: {} } }); server.server.registerCapabilities({ resources: {} }) const PINATA_JWT = process.env.PINATA_JWT; const GATEWAY_URL = process.env.GATEWAY_URL; // Base headers for all requests const getHeaders = () => { if (!PINATA_JWT) { throw new Error("PINATA_JWT environment variable is not set"); } return { Authorization: `Bearer ${PINATA_JWT}`, "Content-Type": "application/json", }; }; server.tool( "searchFiles", { network: z.enum(["public", "private"]).default("public"), name: z.string().optional(), cid: z.string().optional(), mimeType: z.string().optional(), limit: z.number().optional(), }, async ({ network, name, cid, mimeType, limit }) => { try { // Build query parameters const params = new URLSearchParams(); if (name) params.append("name", name); if (cid) params.append("cid", cid); if (mimeType) params.append("mimeType", mimeType); if (limit) params.append("limit", limit.toString()); const url = `https://api.pinata.cloud/v3/files/${network}?${params.toString()}`; const response = await fetch(url, { method: "GET", headers: getHeaders(), }); if (!response.ok) { throw new Error(`Failed to search files: ${response.status} ${response.statusText}`); } const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error}` }], }; } } ); server.tool( "getFileById", { network: z.enum(["public", "private"]).default("public"), id: z.string(), }, async ({ network, id }) => { try { const url = `https://api.pinata.cloud/v3/files/${network}/${id}`; const response = await fetch(url, { method: "GET", headers: getHeaders(), }); if (!response.ok) { throw new Error(`Failed to get file: ${response.status} ${response.statusText}`); } const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error}` }], }; } } ); server.tool( "updateFile", { network: z.enum(["public", "private"]).default("public"), id: z.string(), name: z.string().optional(), keyvalues: z.record(z.any()).optional(), }, async ({ network, id, name, keyvalues }) => { try { const url = `https://api.pinata.cloud/v3/files/${network}/${id}`; const payload: { name?: string; keyvalues?: Record<string, any> } = {}; if (name) payload.name = name; if (keyvalues) payload.keyvalues = keyvalues; const response = await fetch(url, { method: "PUT", headers: getHeaders(), body: JSON.stringify(payload), }); if (!response.ok) { throw new Error(`Failed to update file: ${response.status} ${response.statusText}`); } const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error}` }], }; } } ); server.tool( "deleteFile", { network: z.enum(["public", "private"]).default("public"), id: z.string(), }, async ({ network, id }) => { try { const url = `https://api.pinata.cloud/v3/files/${network}/${id}`; const response = await fetch(url, { method: "DELETE", headers: getHeaders(), }); if (!response.ok) { throw new Error(`Failed to delete file: ${response.status} ${response.statusText}`); } const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error}` }], }; } } ); server.tool( "createPrivateDownloadLink", { cid: z.string(), expires: z.number(), }, async ({ cid, expires }) => { try { const apiUrl = `https://api.pinata.cloud/v3/files/private/download_link`; const url = `https://${GATEWAY_URL}/files/${cid}` const date = Math.floor(new Date().getTime() / 1000) const payload = { url, expires, date, method: "GET", }; const response = await fetch(apiUrl, { method: "POST", headers: getHeaders(), body: JSON.stringify(payload), }); if (!response.ok) { throw new Error(`Failed to create download link: ${response.status} ${response.statusText}`); } const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error}` }], }; } } ); server.tool( "listGroups", { network: z.enum(["public", "private"]).default("public"), name: z.string().optional(), limit: z.number().optional(), pageToken: z.string().optional(), }, async ({ network, name, limit, pageToken }) => { try { const params = new URLSearchParams(); if (name) params.append("name", name); if (limit) params.append("limit", limit.toString()); if (pageToken) params.append("pageToken", pageToken); const url = `https://api.pinata.cloud/v3/groups/${network}?${params.toString()}`; const response = await fetch(url, { method: "GET", headers: getHeaders(), }); if (!response.ok) { throw new Error(`Failed to list groups: ${response.status} ${response.statusText}`); } const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error}` }], }; } } ); server.tool( "createGroup", { network: z.enum(["public", "private"]).default("public"), name: z.string(), }, async ({ network, name }) => { try { const url = `https://api.pinata.cloud/v3/groups/${network}`; const payload: { name: string; is_public?: boolean } = { name, }; const response = await fetch(url, { method: "POST", headers: getHeaders(), body: JSON.stringify(payload), }); if (!response.ok) { throw new Error(`Failed to create group: ${response.status} ${response.statusText}`); } const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error}` }], }; } } ); server.tool( "getGroup", { network: z.enum(["public", "private"]).default("public"), id: z.string(), }, async ({ network, id }) => { try { const url = `https://api.pinata.cloud/v3/groups/${network}/${id}`; const response = await fetch(url, { method: "GET", headers: getHeaders(), }); if (!response.ok) { throw new Error(`Failed to get group: ${response.status} ${response.statusText}`); } const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error}` }], }; } } ); server.tool( "updateGroup", { network: z.enum(["public", "private"]).default("public"), id: z.string(), name: z.string().optional(), }, async ({ network, id, name }) => { try { const url = `https://api.pinata.cloud/v3/groups/${network}/${id}`; const payload: { name?: string; is_public?: boolean } = {}; if (name) payload.name = name; const response = await fetch(url, { method: "PUT", headers: getHeaders(), body: JSON.stringify(payload), }); if (!response.ok) { throw new Error(`Failed to update group: ${response.status} ${response.statusText}`); } const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error}` }], }; } } ); server.tool( "deleteGroup", { network: z.enum(["public", "private"]).default("public"), id: z.string(), }, async ({ network, id }) => { try { const url = `https://api.pinata.cloud/v3/groups/${network}/${id}`; const response = await fetch(url, { method: "DELETE", headers: getHeaders(), }); if (!response.ok) { throw new Error(`Failed to delete group: ${response.status} ${response.statusText}`); } const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error}` }], }; } } ); server.tool( "addFileToGroup", { network: z.enum(["public", "private"]).default("public"), groupId: z.string(), fileId: z.string(), }, async ({ network, groupId, fileId }) => { try { const url = `https://api.pinata.cloud/v3/groups/${network}/${groupId}/ids/${fileId}`; const response = await fetch(url, { method: "PUT", headers: getHeaders(), }); if (!response.ok) { throw new Error(`Failed to add file to group: ${response.status} ${response.statusText}`); } const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error}` }], }; } } ); server.tool( "removeFileFromGroup", { network: z.enum(["public", "private"]).default("public"), groupId: z.string(), fileId: z.string(), }, async ({ network, groupId, fileId }) => { try { const url = `https://api.pinata.cloud/v3/groups/${network}/${groupId}/ids/${fileId}`; const response = await fetch(url, { method: "DELETE", headers: getHeaders(), }); if (!response.ok) { throw new Error(`Failed to remove file from group: ${response.status} ${response.statusText}`); } const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error}` }], }; } } ); // Upload file to Pinata server.tool( "uploadFile", { resourceUri: z.string().optional().describe("The file:// URI of the file to upload"), fileContent: z.string().optional().describe("Base64-encoded file content (required in Claude Desktop)"), mimeType: z.string().optional().describe("MIME type of the file"), network: z.enum(["public", "private"]).default("public"), name: z.string().optional(), group_id: z.string().optional(), keyvalues: z.record(z.string()).optional(), }, async ({ resourceUri, fileContent, mimeType, network, name, group_id, keyvalues }) => { try { let fileBuffer: Buffer; let fileName: string; // Handle two different modes of operation: if (fileContent && !resourceUri) { // Direct content mode (for Claude Desktop) fileBuffer = Buffer.from(fileContent, 'base64'); fileName = name || 'uploaded-file'; } else if (resourceUri) { // File path mode (for project environments) if (!resourceUri.startsWith("file://")) { throw new Error("Resource URI must be a file:// URI when not providing direct file content"); } // Extract file path from URI let filePath; if (process.platform === 'win32') { filePath = decodeURIComponent(resourceUri.replace(/^file:\/\/\//, '').replace(/\//g, '\\')); } else { filePath = decodeURIComponent(resourceUri.replace(/^file:\/\//, '')); } try { // Validate path is allowed filePath = await validatePath(filePath); fileBuffer = await fs.readFile(filePath); fileName = name || path.basename(filePath); } catch (err) { return { content: [{ type: "text", text: `Error: Cannot read file at ${filePath}. Current working directory: ${process.cwd()} Error details: ${err} Note: When using Claude Desktop, you must provide fileContent parameter with base64-encoded file data.` }], isError: true }; } } else { // Neither resourceUri nor fileContent provided throw new Error("Either resourceUri or fileContent must be provided"); } // Determine MIME type if not provided const detectedMimeType = mimeType || getMimeType(fileName); // Create form data for the upload const formData = new FormData(); const blob = new Blob([fileBuffer], { type: detectedMimeType }); formData.append("file", blob, fileName); formData.append("network", network); if (name) { formData.append("name", name); } if (group_id) { formData.append("group_id", group_id); } if (keyvalues) { formData.append("keyvalues", JSON.stringify(keyvalues)); } // Send request to Pinata 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 { content: [{ type: "text", text: `Error uploading file: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( "createLink", { cid: z.string().describe("The CID of the file to create a link for"), network: z.enum(["public", "private"]).default("public").describe("Whether the file is on public or private IPFS"), expires: z.number().optional().default(600).describe("Expiration time in seconds for private download links") }, async ({ cid, network, expires = 600 }) => { try { if (!GATEWAY_URL) { throw new Error("GATEWAY_URL environment variable is not set"); } let fileUrl; if (network === "public") { // For public IPFS, we can access directly through the gateway fileUrl = `https://${GATEWAY_URL}/ipfs/${cid}`; return { content: [{ type: "text", text: `✅ Public IPFS link created:\n${fileUrl}` }], }; } else { // For private IPFS, we need to create a download link if (!PINATA_JWT) { throw new Error("PINATA_JWT environment variable is not set"); } // The URL for the download link API const filePath = `https://${GATEWAY_URL}/files/${cid}`; const apiUrl = `https://api.pinata.cloud/v3/files/private/download_link`; // Current timestamp and provided expiration const date = Math.floor(new Date().getTime() / 1000); const payload = { url: filePath, expires, date, method: "GET" }; // Create the download link const linkResponse = await fetch(apiUrl, { method: "POST", headers: getHeaders(), body: JSON.stringify(payload) }); if (!linkResponse.ok) { const errorText = await linkResponse.text(); throw new Error(`Failed to create download link: ${linkResponse.status} ${linkResponse.statusText}. Response: ${errorText}`); } const linkData = await linkResponse.json(); if (!linkData.data) { throw new Error("Failed to get download URL from response"); } fileUrl = linkData.data; const expirationTime = new Date((date + expires) * 1000).toLocaleString(); return { content: [{ type: "text", text: `✅ Private IPFS temporary link created:\n${fileUrl}\n\nThis link will expire at: ${expirationTime} (${expires} seconds from creation)` }], }; } } catch (error) { return { content: [{ type: "text", text: `Error creating link: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( "fetchFromGateway", { cid: z.string().describe("The CID of the file to fetch"), network: z.enum(["public", "private"]).default("public").describe("Whether the file is on public or private IPFS"), saveToPath: z.string().optional().describe("Optional local file path to save the fetched content"), returnContent: z.boolean().default(false).describe("Whether to return the content directly (not recommended for large files)") }, async ({ cid, network, saveToPath, returnContent }) => { try { if (!GATEWAY_URL) { throw new Error("GATEWAY_URL environment variable is not set"); } let fileUrl; let response; if (network === "public") { // For public IPFS, we can access directly through the gateway fileUrl = `https://${GATEWAY_URL}/ipfs/${cid}`; response = await fetch(fileUrl); if (!response.ok) { throw new Error(`Failed to fetch public file: ${response.status} ${response.statusText}`); } } else { // For private IPFS, we need to create a download link first if (!PINATA_JWT) { throw new Error("PINATA_JWT environment variable is not set"); } // The URL for the download link API const filePath = `https://${GATEWAY_URL}/files/${cid}`; const apiUrl = `https://api.pinata.cloud/v3/files/private/download_link`; // Current timestamp and expiration (e.g., 10 minutes from now) const date = Math.floor(new Date().getTime() / 1000); const expires = 600; // 10 minutes const payload = { url: filePath, expires, date, method: "GET" }; // Create the download link const linkResponse = await fetch(apiUrl, { method: "POST", headers: getHeaders(), body: JSON.stringify(payload) }); if (!linkResponse.ok) { const errorText = await linkResponse.text(); throw new Error(`Failed to create download link: ${linkResponse.status} ${linkResponse.statusText}. Response: ${errorText}`); } const linkData = await linkResponse.json(); if (!linkData.data) { throw new Error("Failed to get download URL from response"); } // Use the generated download link fileUrl = linkData.data; response = await fetch(fileUrl); if (!response.ok) { throw new Error(`Failed to fetch private file: ${response.status} ${response.statusText}`); } } // Get content type from headers const contentType = response.headers.get("content-type") || "application/octet-stream"; // Handle the response based on the options const arrayBuffer = await response.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); // If a save path is provided, save the file if (saveToPath) { // Validate the save path is allowed const validatedPath = await validatePath(saveToPath); // Create directory if it doesn't exist const dir = path.dirname(validatedPath); await fs.mkdir(dir, { recursive: true }); // Save the file await fs.writeFile(validatedPath, buffer); } // Return appropriate response let resultText = ""; if (network === "public") { resultText = `✅ Fetched ${buffer.length} bytes from public IPFS (CID: ${cid})`; } else { resultText = `✅ Fetched ${buffer.length} bytes from private IPFS (CID: ${cid})`; } if (saveToPath) { resultText += `\nFile saved to: ${saveToPath}`; } // Return content if requested and if it's reasonable to do so if (returnContent) { // If it's a text file and not too large, return as text if (contentType.startsWith("text/") || contentType.includes("json") || contentType.includes("javascript") || contentType.includes("xml")) { if (buffer.length < 100000) { // Less than 100KB const textContent = buffer.toString('utf-8'); resultText += `\n\nContent:\n${textContent}`; } else { resultText += `\n\nContent too large to display (${buffer.length} bytes). File was saved${saveToPath ? ` to ${saveToPath}` : ' but not returned'}.`; } } else if (buffer.length < 50000) { // Less than 50KB for binary files // For small binary files, return base64 resultText += `\n\nBase64 Content (${contentType}):\n${buffer.toString('base64')}`; } else { resultText += `\n\nContent too large to display (${buffer.length} bytes). File was saved${saveToPath ? ` to ${saveToPath}` : ' but not returned'}.`; } } return { content: [{ type: "text", text: resultText }], }; } catch (error) { return { content: [{ type: "text", text: `Error fetching file: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // Add a tool to list allowed directories server.tool( "listAllowedDirectories", {}, async () => { return { content: [{ type: "text", text: `Allowed directories:\n${allowedDirectories.join('\n')}` }], }; } ); // List available resources server.server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: [ { uriTemplate: "file://{path}", name: "Local Files", description: "Access local files to upload to Pinata IPFS (only from allowed directories)" } ] }; }); // Read resource contents server.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri; // Handle file resource if (uri.startsWith("file://")) { let filePath = decodeURIComponent(uri.replace(/^file:\/\//, '')); if (process.platform === 'win32') { filePath = filePath.replace(/\//g, '\\'); } try { // Validate path is allowed filePath = await validatePath(filePath); // Check if the file exists const fileStats = await fs.stat(filePath); if (!fileStats.isFile()) { throw new Error(`Not a file: ${filePath}`); } const mimeType = getMimeType(filePath); // For text files, read as text if (isTextFile(mimeType)) { const content = await fs.readFile(filePath, 'utf-8'); return { contents: [ { uri, mimeType, text: content } ] }; } else { // For binary files, use base64 encoding const content = await fs.readFile(filePath); return { contents: [ { uri, mimeType, blob: content.toString('base64') } ] }; } } catch (error) { throw new Error(`Failed to read file: ${error}`); } } throw new Error("Unsupported resource URI"); }); server.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "searchFiles", description: "Search for files in your Pinata account by name, CID, or MIME type. Returns a list of files matching the given criteria.", inputSchema: { type: "object", properties: { network: { type: "string", enum: ["public", "private"], description: "Whether to search in public or private IPFS", default: "public" }, name: { type: "string", description: "Search by filename (optional)" }, cid: { type: "string", description: "Search by content ID (optional)" }, mimeType: { type: "string", description: "Search by MIME type (optional)" }, limit: { type: "number", description: "Maximum number of results to return (optional)" } } }, }, { name: "getFileById", description: "Retrieve detailed information about a specific file stored on Pinata by its ID.", inputSchema: { type: "object", properties: { network: { type: "string", enum: ["public", "private"], description: "Whether the file is in public or private IPFS", default: "public" }, id: { type: "string", description: "The ID of the file to retrieve" } }, required: ["id"] }, }, { name: "updateFile", description: "Update metadata for an existing file on Pinata. Can update the name and keyvalues (metadata).", inputSchema: { type: "object", properties: { network: { type: "string", enum: ["public", "private"], description: "Whether the file is in public or private storage", default: "public" }, id: { type: "string", description: "The ID of the file to update" }, name: { type: "string", description: "New name for the file (optional)" }, keyvalues: { type: "object", description: "Metadata key-value pairs to update (optional)" } }, required: ["id"] }, }, { name: "deleteFile", description: "Delete a file from your Pinata account by its ID.", inputSchema: { type: "object", properties: { network: { type: "string", enum: ["public", "private"], description: "Whether the file is in public or private IPFS", default: "public" }, id: { type: "string", description: "The ID of the file to delete" } }, required: ["id"] }, }, { name: "createPrivateDownloadLink", description: "Generate a temporary download link for accessing a private ipfs file from Pinata.", inputSchema: { type: "object", properties: { cid: { type: "string", description: "The content ID (CID) of the file" }, expires: { type: "number", description: "Expiration time in seconds for the download link" } }, required: ["cid", "expires"] }, }, { name: "createLink", description: "Create a direct access link for a file stored on Pinata IPFS. For private files, generates a temporary download link.", inputSchema: { type: "object", properties: { cid: { type: "string", description: "The CID of the file to create a link for" }, network: { type: "string", enum: ["public", "private"], description: "Whether the file is on public or private IPFS", default: "public" }, expires: { type: "number", description: "Expiration time in seconds for private download links", default: 600 } }, required: ["cid"] }, }, { name: "listGroups", description: "List groups in your Pinata account, with optional filtering by name.", inputSchema: { type: "object", properties: { network: { type: "string", enum: ["public", "private"], description: "Whether to list groups in public or private IPFS", default: "public" }, name: { type: "string", description: "Filter groups by name (optional)" }, isPublic: { type: "boolean", description: "Filter groups by public status (optional)" }, limit: { type: "number", description: "Maximum number of results to return (optional)" }, pageToken: { type: "string", description: "Token for pagination (optional)" } } }, }, { name: "createGroup", description: "Create a new group in your Pinata account to organize files.", inputSchema: { type: "object", properties: { network: { type: "string", enum: ["public", "private"], description: "Whether to create the group in public or private IPFS", default: "public" }, name: { type: "string", description: "Name for the new group" }, is_public: { type: "boolean", description: "Whether the group should be public (optional)" } }, required: ["name"] }, }, { name: "getGroup", description: "Retrieve detailed information about a specific group by its ID.", inputSchema: { type: "object", properties: { network: { type: "string", enum: ["public", "private"], description: "Whether the group is in public or private IPFS", default: "public" }, id: { type: "string", description: "The ID of the group to retrieve" } }, required: ["id"] }, }, { name: "updateGroup", description: "Update metadata for an existing group on Pinata. Can update the name.", inputSchema: { type: "object", properties: { network: { type: "string", enum: ["public", "private"], description: "Whether the group is in public or private IPFS", default: "public" }, id: { type: "string", description: "The ID of the group to update" }, name: { type: "string", description: "New name for the group (optional)" }, }, required: ["id"] }, }, { name: "deleteGroup", description: "Delete a group from your Pinata storage by its ID.", inputSchema: { type: "object", properties: { network: { type: "string", enum: ["public", "private"], description: "Whether the group is in public or private IPFS", default: "public" }, id: { type: "string", description: "The ID of the group to delete" } }, required: ["id"] }, }, { name: "addFileToGroup", description: "Add an existing file to a group in your Pinata account.", inputSchema: { type: "object", properties: { network: { type: "string", enum: ["public", "private"], description: "Whether the group and file are in public or private IPFS", default: "public" }, groupId: { type: "string", description: "The ID of the group to add the file to" }, fileId: { type: "string", description: "The ID of the file to add to the group" } }, required: ["groupId", "fileId"] }, }, { name: "removeFileFromGroup", description: "Remove a file from a group in your Pinata account.", inputSchema: { type: "object", properties: { network: { type: "string", enum: ["public", "private"], description: "Whether the group and file are in public or private IPFS", default: "public" }, groupId: { type: "string", description: "The ID of the group to remove the file from" }, fileId: { type: "string", description: "The ID of the file to remove from the group" } }, required: ["groupId", "fileId"] }, }, { name: "uploadFile", description: "Upload a file to Pinata Public of Private IPFS. Can upload from local filesystem or directly from provided content.", inputSchema: { type: "object", properties: { resourceUri: { type: "string", description: "The file:// URI of the file to upload (optional if providing fileContent)" }, fileContent: { type: "string", description: "Base64-encoded file content (required in Claude Desktop or when not using resourceUri)" }, mimeType: { type: "string", description: "MIME type of the file (optional, will be detected from filename if not provided)" }, network: { type: "string", enum: ["public", "private"], description: "Whether to upload to public or private IPFS", default: "public" }, name: { type: "string", description: "Custom name for the file (optional)" }, group_id: { type: "string", description: "ID of a group to add the file to (optional)" }, keyvalues: { type: "object", description: "Metadata key-value pairs for the file (optional)" } } }, }, { name: "fetchFromGateway", description: "Fetch a file from Public or Private IPFS via Pinata gateway. Can retrieve public and private files and optionally save to local filesystem.", inputSchema: { type: "object", properties: { cid: { type: "string", description: "The CID of the file to fetch" }, network: { type: "string", enum: ["public", "private"], description: "Whether the file is on public or private IPFS", default: "public" }, saveToPath: { type: "string", description: "Optional local file path to save the fetched content" }, returnContent: { type: "boolean", description: "Whether to return the content directly (not recommended for large files)", default: false } }, required: ["cid"] }, }, { name: "listAllowedDirectories", description: "List all directories that this MCP server is allowed to access for file operations.", inputSchema: { type: "object", properties: {}, required: [] }, } ], }; }); // Helper function to get MIME type from file extension 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', 'mp3': 'audio/mpeg', 'mp4': 'video/mp4', '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'; } // Helper function to determine if a file is text-based function isTextFile(mimeType: string): boolean { return mimeType.startsWith('text/') || mimeType === 'application/json' || mimeType === 'application/javascript' || mimeType === 'application/xml'; } async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Pinata MCP Server running on stdio"); console.error("Allowed directories:", allowedDirectories); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); });