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));
        }
      }
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden for behavioral disclosure but provides minimal information. It mentions downloading but doesn't cover important aspects like authentication requirements, rate limits, file size considerations, network behavior, or what happens if the model isn't downloadable. The description is functionally correct but lacks operational context.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that states the core function without unnecessary words. It's appropriately sized for a straightforward download operation and gets directly to the point with zero wasted verbiage.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a download tool with no annotations and no output schema, the description is insufficient. It doesn't explain what gets returned (file path? success status?), error conditions, authentication needs, or important constraints like the 'must be downloadable' requirement mentioned in the schema. The combination of missing behavioral context and output information creates significant gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 100% schema description coverage, the baseline is 3. The description doesn't add any parameter semantics beyond what's already in the schema - it doesn't explain what makes a model 'downloadable' (hinted in schema), provide format selection guidance, or clarify output path behavior. The schema already documents all three parameters thoroughly.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the verb ('download') and resource ('3D model from Sketchfab'), making the purpose immediately understandable. However, it doesn't differentiate from sibling tools like 'sketchfab-model-details' or 'sketchfab-search' - all could involve downloading or accessing models, so the distinction isn't explicit.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance is provided about when to use this tool versus alternatives like 'sketchfab-model-details' (which might provide metadata before downloading) or 'sketchfab-search' (which might help find models first). The description only states what the tool does, not when it's appropriate to invoke it.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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