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
| Name | Required | Description | Default |
|---|---|---|---|
| modelId | Yes | The unique ID of the Sketchfab model to download (must be downloadable) | |
| format | No | Preferred format to download the model in (defaults to gltf if available) | |
| outputPath | No | Local directory or file path to save the downloaded file (will use temp directory if not specified) |
Implementation Reference
- index.ts:429-582 (handler)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}`, }, ], }; } }
- index.ts:424-428 (schema)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",
- index.ts:70-206 (helper)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)); } } }