UNPKG

sketchfab-mcp

Version:

A Model Context Protocol (MCP) server for interacting with Sketchfab

407 lines (406 loc) 15.4 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import axios from "axios"; import fs from "fs"; import path from "path"; import os from "os"; // Parse command line arguments const args = process.argv.slice(2); let apiKey = ""; // Extract API key from command line arguments for (let i = 0; i < args.length; i++) { if (args[i] === "--api-key" && i + 1 < args.length) { apiKey = args[i + 1]; break; } } // Use environment variable as fallback if (!apiKey) { apiKey = process.env.SKETCHFAB_API_KEY || ""; } // Create server instance const server = new McpServer({ name: "3d-model-mcp-server", version: "0.0.1", }); // Simplified Sketchfab API client class SketchfabApiClient { constructor(apiKey) { this.apiKey = apiKey; } getAuthHeader() { return { Authorization: `Token ${this.apiKey}`, }; } async searchModels(options) { try { const { q, tags, categories, downloadable, count = 24 } = options; // Build query parameters const params = { 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) { 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) { try { const response = await axios.get(`${SketchfabApiClient.API_BASE}/models/${uid}`, { headers: this.getAuthHeader(), }); return response.data; } catch (error) { 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) { try { const response = await axios.get(`${SketchfabApiClient.API_BASE}/models/${uid}/download`, { headers: this.getAuthHeader(), }); return response.data; } catch (error) { 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) { try { const response = await axios.get(downloadUrl, { responseType: "arraybuffer", timeout: 300000, // 5 minutes }); return Buffer.from(response.data); } catch (error) { 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)); } } } SketchfabApiClient.API_BASE = "https://api.sketchfab.com/v3"; // Helper function to format model for display function formatModelForDisplay(model) { const thumbnailUrl = model.thumbnails?.images?.[0]?.url || "No thumbnail"; const username = model.user?.username || "Unknown"; const downloadable = model.isDownloadable ? "Yes" : "No"; return ` [Model] ${model.name} ID: ${model.uid} Creator: ${username} Downloadable: ${downloadable} Thumbnail: ${thumbnailUrl} ${model.description ? `Description: ${model.description}` : ""} `; } // Define a sample tool server.tool("sample-tool", "A sample tool for demonstration purposes", { input: z.string().describe("Input parameter for the sample tool"), }, async ({ input }) => { // Process the input const output = `Processed: ${input}`; // Return the result return { content: [ { type: "text", text: output, }, ], }; }); // Sketchfab Search Tool server.tool("sketchfab-search", "Search for 3D models on Sketchfab based on keywords and filters", { query: z.string().optional().describe("Text search query (e.g., \"car\", \"house\", \"character\") to find relevant models"), tags: z.array(z.string()).optional().describe("Filter by specific tags (e.g., [\"animated\", \"rigged\", \"pbr\"])"), categories: z.array(z.string()).optional().describe("Filter by categories (e.g., [\"characters\", \"architecture\", \"vehicles\"])"), downloadable: z.boolean().optional().describe("Set to true to show only downloadable models, false to show all models"), limit: z.number().optional().describe("Maximum number of results to return (1-24, default: 10)"), }, async ({ query, tags, categories, downloadable, limit }) => { try { // Validate input if (!query && (!tags || tags.length === 0) && (!categories || categories.length === 0)) { return { content: [ { type: "text", text: "Please provide at least one search parameter: query, tags, or categories.", }, ], }; } // 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); // Search for models const searchResults = await client.searchModels({ q: query, tags, categories, downloadable, count: limit || 10, }); // Handle no results if (!searchResults.results || searchResults.results.length === 0) { return { content: [ { type: "text", text: "No models found matching your search criteria. Try different keywords or filters.", }, ], }; } // Format results const formattedResults = searchResults.results .map((model, index) => `[${index + 1}] ${model.name}\nID: ${model.uid}\nDownloadable: ${model.isDownloadable ? "Yes" : "No"}\n`) .join("\n"); return { content: [ { type: "text", text: `Found ${searchResults.results.length} models:\n\n${formattedResults}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Error searching Sketchfab: ${errorMessage}`, }, ], }; } }); // Sketchfab Model Details Tool server.tool("sketchfab-model-details", "Get detailed information about a specific Sketchfab model", { modelId: z.string().describe("The unique ID of the Sketchfab model (found in URLs or search results)"), }, async ({ modelId }) => { 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); // Format model details const formattedModel = formatModelForDisplay(model); return { content: [ { type: "text", text: formattedModel, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Error getting model details: ${errorMessage}`, }, ], }; } }); // Sketchfab Download Tool 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; if (!downloadLinks[requestedFormat]) { // Find available formats const availableFormats = Object.keys(downloadLinks).filter((key) => downloadLinks[key]); 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]; 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); // Write the file 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); // Write the file fs.writeFileSync(savePath, modelData); return { content: [ { type: "text", text: `Downloaded model "${model.name}" in ${format} format.\nSaved to: ${savePath}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Error downloading model: ${errorMessage}`, }, ], }; } }); async function main() { // Log API key status if (apiKey) { console.log("Sketchfab API key provided"); } else { console.log("No Sketchfab API key provided. Some functionality may be limited."); } const transport = new StdioServerTransport(); await server.connect(transport); console.log("MCP Server running on stdio"); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); });