Skip to main content
Glama
gregkop

Sketchfab MCP Server

by gregkop

sketchfab-download

Download 3D models from Sketchfab by providing a model ID and preferred format (gltf, glb, usdz, or source), saving files locally for use in projects.

Instructions

Download a 3D model from Sketchfab

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
modelIdYesThe unique ID of the Sketchfab model to download (must be downloadable)
formatNoPreferred format to download the model in (defaults to gltf if available)
outputPathNoLocal directory or file path to save the downloaded file (will use temp directory if not specified)

Implementation Reference

  • The async handler function implementing the core logic for the 'sketchfab-download' tool. It checks for API key, fetches model info, retrieves download URL for the specified format (fallback if unavailable), downloads the binary data, detects if ZIP and extracts it using adm-zip, saves files/ZIP to outputPath or system temp dir, and returns markdown text with file paths.
    async ({ modelId, format = "gltf", outputPath }) => {
      try {
        // Check if API key is available
        if (!apiKey) {
          return {
            content: [
              {
                type: "text",
                text: "No Sketchfab API key provided. Please provide an API key using the --api-key parameter or set the SKETCHFAB_API_KEY environment variable.",
              },
            ],
          };
        }
    
        // Create API client
        const client = new SketchfabApiClient(apiKey);
        
        // Get model details
        const model = await client.getModel(modelId);
        
        // Check if model is downloadable
        if (!model.isDownloadable) {
          return {
            content: [
              {
                type: "text",
                text: `Model "${model.name}" is not downloadable.`,
              },
            ],
          };
        }
        
        // Get download links
        const downloadLinks = await client.getModelDownloadLink(modelId);
        
        // Check if requested format is available
        const requestedFormat = format as keyof typeof downloadLinks;
        if (!downloadLinks[requestedFormat]) {
          // Find available formats
          const availableFormats = Object.keys(downloadLinks).filter(
            (key) => downloadLinks[key as keyof typeof downloadLinks]
          );
          
          if (availableFormats.length === 0) {
            return {
              content: [
                {
                  type: "text",
                  text: "No download formats available for this model.",
                },
              ],
            };
          }
          
          // Use the first available format
          const fallbackFormat = availableFormats[0] as keyof typeof downloadLinks;
          const fallbackLink = downloadLinks[fallbackFormat]!;
          
          // Download the model
          const modelData = await client.downloadModel(fallbackLink.url);
          
          // Determine filename and path
          const filename = `${model.name.replace(/[^a-zA-Z0-9]/g, "_")}_${modelId}.${fallbackFormat}`;
          const savePath = outputPath || path.join(os.tmpdir(), filename);
          const saveDir = path.dirname(savePath);
          
          // Check if the downloaded file is a ZIP archive
          if (isZipFile(modelData)) {
            // Create a directory for extraction
            const extractDir = path.join(saveDir, `${path.basename(savePath, path.extname(savePath))}_extracted`);
            
            // Extract the ZIP file
            const extractedFiles = extractZipFile(modelData, extractDir);
            
            // Save the original ZIP file as well
            fs.writeFileSync(savePath, modelData);
            
            return {
              content: [
                {
                  type: "text",
                  text: `Downloaded model "${model.name}" in ${fallbackFormat} format (requested ${format} was not available).\nThe file was a ZIP archive and has been automatically extracted.\nOriginal ZIP saved to: ${savePath}\nExtracted files in: ${extractDir}\nExtracted ${extractedFiles.length} files.`,
                },
              ],
            };
          } else {
            // Write the file as is
            fs.writeFileSync(savePath, modelData);
            
            return {
              content: [
                {
                  type: "text",
                  text: `Downloaded model "${model.name}" in ${fallbackFormat} format (requested ${format} was not available).\nSaved to: ${savePath}`,
                },
              ],
            };
          }
        }
        
        // Download the model in the requested format
        const downloadUrl = downloadLinks[requestedFormat]!.url;
        const modelData = await client.downloadModel(downloadUrl);
        
        // Determine filename and path
        const filename = `${model.name.replace(/[^a-zA-Z0-9]/g, "_")}_${modelId}.${format}`;
        const savePath = outputPath || path.join(os.tmpdir(), filename);
        const saveDir = path.dirname(savePath);
        
        // Check if the downloaded file is a ZIP archive
        if (isZipFile(modelData)) {
          // Create a directory for extraction
          const extractDir = path.join(saveDir, `${path.basename(savePath, path.extname(savePath))}_extracted`);
          
          // Extract the ZIP file
          const extractedFiles = extractZipFile(modelData, extractDir);
          
          // Save the original ZIP file as well
          fs.writeFileSync(savePath, modelData);
          
          return {
            content: [
              {
                type: "text",
                text: `Downloaded model "${model.name}" in ${format} format.\nThe file was a ZIP archive and has been automatically extracted.\nOriginal ZIP saved to: ${savePath}\nExtracted files in: ${extractDir}\nExtracted ${extractedFiles.length} files.`,
              },
            ],
          };
        } else {
          // Write the file as is
          fs.writeFileSync(savePath, modelData);
          
          return {
            content: [
              {
                type: "text",
                text: `Downloaded model "${model.name}" in ${format} format.\nSaved to: ${savePath}`,
              },
            ],
          };
        }
      } catch (error: unknown) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        
        return {
          content: [
            {
              type: "text",
              text: `Error downloading model: ${errorMessage}`,
            },
          ],
        };
      }
    }
  • Zod input schema defining parameters for the sketchfab-download tool: modelId (string, required), format (enum ["gltf","glb","usdz","source"], optional, defaults to gltf), outputPath (string, optional).
    {
      modelId: z.string().describe("The unique ID of the Sketchfab model to download (must be downloadable)"),
      format: z.enum(["gltf", "glb", "usdz", "source"]).optional().describe("Preferred format to download the model in (defaults to gltf if available)"),
      outputPath: z.string().optional().describe("Local directory or file path to save the downloaded file (will use temp directory if not specified)"),
    },
  • index.ts:421-423 (registration)
    MCP server.tool registration for the 'sketchfab-download' tool, specifying the tool name and description.
    server.tool(
      "sketchfab-download",
      "Download a 3D model from Sketchfab",
  • SketchfabApiClient class providing essential API interactions for the download tool, including getModelDownloadLink (157-188) to fetch temporary download URLs and downloadModel (190-205) to fetch the binary data.
    class SketchfabApiClient {
      private apiKey: string;
      private static API_BASE = "https://api.sketchfab.com/v3";
    
      constructor(apiKey: string) {
        this.apiKey = apiKey;
      }
    
      private getAuthHeader() {
        return {
          Authorization: `Token ${this.apiKey}`,
        };
      }
    
      async searchModels(options: {
        q?: string;
        tags?: string[];
        categories?: string[];
        downloadable?: boolean;
        count?: number;
      }): Promise<{
        results: SketchfabModel[];
        next?: string;
        previous?: string;
      }> {
        try {
          const { q, tags, categories, downloadable, count = 24 } = options;
          
          // Build query parameters
          const params: Record<string, any> = { type: "models" };
          
          if (q) params.q = q;
          if (tags?.length) params.tags = tags;
          if (categories?.length) params.categories = categories;
          if (downloadable !== undefined) params.downloadable = downloadable;
          if (count) params.count = Math.min(count, 24); // API limit is 24
          
          // Make API request
          const response = await axios.get(`${SketchfabApiClient.API_BASE}/search`, {
            params,
            headers: this.getAuthHeader(),
          });
          
          return {
            results: response.data.results || [],
            next: response.data.next,
            previous: response.data.previous,
          };
        } catch (error: unknown) {
          if (axios.isAxiosError(error) && error.response) {
            const status = error.response.status;
            
            if (status === 401) {
              throw new Error("Invalid Sketchfab API key");
            } else if (status === 429) {
              throw new Error("Sketchfab API rate limit exceeded. Try again later.");
            }
            throw new Error(`Sketchfab API error (${status}): ${error.message}`);
          }
          throw error instanceof Error ? error : new Error(String(error));
        }
      }
    
      async getModel(uid: string): Promise<SketchfabModel> {
        try {
          const response = await axios.get(
            `${SketchfabApiClient.API_BASE}/models/${uid}`,
            {
              headers: this.getAuthHeader(),
            }
          );
          
          return response.data;
        } catch (error: unknown) {
          if (axios.isAxiosError(error) && error.response) {
            const status = error.response.status;
            if (status === 404) {
              throw new Error(`Model with UID ${uid} not found`);
            } else if (status === 401) {
              throw new Error("Invalid Sketchfab API key");
            }
            throw new Error(`Sketchfab API error (${status}): ${error.message}`);
          }
          throw error instanceof Error ? error : new Error(String(error));
        }
      }
    
      async getModelDownloadLink(uid: string): Promise<{
        gltf?: { url: string; expires: number };
        usdz?: { url: string; expires: number };
        glb?: { url: string; expires: number };
        source?: { url: string; expires: number };
      }> {
        try {
          const response = await axios.get(
            `${SketchfabApiClient.API_BASE}/models/${uid}/download`,
            {
              headers: this.getAuthHeader(),
            }
          );
          
          return response.data;
        } catch (error: unknown) {
          if (axios.isAxiosError(error) && error.response) {
            const status = error.response.status;
            if (status === 404) {
              throw new Error(`Model with UID ${uid} not found`);
            } else if (status === 401) {
              throw new Error("Invalid Sketchfab API key");
            } else if (status === 400) {
              throw new Error("Model is not downloadable");
            } else if (status === 403) {
              throw new Error("You do not have permission to download this model");
            }
            throw new Error(`Sketchfab API error (${status}): ${error.message}`);
          }
          throw error instanceof Error ? error : new Error(String(error));
        }
      }
    
      async downloadModel(downloadUrl: string): Promise<Buffer> {
        try {
          const response = await axios.get(downloadUrl, {
            responseType: "arraybuffer",
            timeout: 300000, // 5 minutes
          });
          
          return Buffer.from(response.data);
        } catch (error: unknown) {
          if (axios.isAxiosError(error) && error.response) {
            const status = error.response.status;
            throw new Error(`Download error (${status}): ${error.message}`);
          }
          throw error instanceof Error ? error : new Error(String(error));
        }
      }
    }

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/gregkop/sketchfab-mcp-server'

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