sketchfab-download
Download 3D models from Sketchfab in formats like gltf, glb, usdz, or source. Specify the model ID and output path to save directly to your desired location.
Instructions
Download a 3D model from Sketchfab
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| format | No | Preferred format to download the model in (defaults to gltf if available) | |
| modelId | Yes | The unique ID of the Sketchfab model to download (must be downloadable) | |
| outputPath | No | Local directory or file path to save the downloaded file (will use temp directory if not specified) |
Implementation Reference
- index.ts:421-583 (registration)Registration of the 'sketchfab-download' tool using server.tool(), which includes the description, input schema, and inline asynchronous handler function that performs the download.server.tool( "sketchfab-download", "Download a 3D model from Sketchfab", { 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)"), }, 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 schema defining input parameters for the tool: modelId (required), format (optional enum), outputPath (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:429-582 (handler)Inline async handler function that implements the core logic: API interactions via SketchfabApiClient, format selection/fallback, download, ZIP detection and extraction, file saving, and MCP content response.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:70-206 (helper)Supporting SketchfabApiClient class with methods getModel, getModelDownloadLink, and downloadModel used directly in the handler for model info, download links, and file retrieval.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)); } } }
- index.ts:209-237 (helper)Utility functions isZipFile and extractZipFile for detecting and automatically extracting ZIP archives (common for Sketchfab downloads).function isZipFile(buffer: Buffer): boolean { // Check for ZIP file signature (PK..) return buffer.length >= 4 && buffer[0] === 0x50 && buffer[1] === 0x4B && (buffer[2] === 0x03 || buffer[2] === 0x05 || buffer[2] === 0x07) && (buffer[3] === 0x04 || buffer[3] === 0x06 || buffer[3] === 0x08); } // Utility function to extract a ZIP file function extractZipFile(zipBuffer: Buffer, outputDir: string): string[] { try { const zip = new AdmZip(zipBuffer); const zipEntries = zip.getEntries(); // Create output directory if it doesn't exist if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } // Extract all files zip.extractAllTo(outputDir, true); // Return list of extracted files return zipEntries.map(entry => path.join(outputDir, entry.entryName)); } catch (error) { throw new Error(`Failed to extract ZIP file: ${error instanceof Error ? error.message : String(error)}`); } }