Add Sketchfab integration

This commit is contained in:
Sahil Siddiki 2025-05-09 10:50:02 +05:30
parent 972096e9dc
commit 8b7e89fbcd
2 changed files with 425 additions and 3 deletions

271
addon.py
View File

@ -11,6 +11,7 @@ import tempfile
import traceback import traceback
import os import os
import shutil import shutil
import zipfile
from bpy.props import StringProperty, IntProperty, BoolProperty, EnumProperty from bpy.props import StringProperty, IntProperty, BoolProperty, EnumProperty
import io import io
from contextlib import redirect_stdout from contextlib import redirect_stdout
@ -200,6 +201,7 @@ class BlenderMCPServer:
"execute_code": self.execute_code, "execute_code": self.execute_code,
"get_polyhaven_status": self.get_polyhaven_status, "get_polyhaven_status": self.get_polyhaven_status,
"get_hyper3d_status": self.get_hyper3d_status, "get_hyper3d_status": self.get_hyper3d_status,
"get_sketchfab_status": self.get_sketchfab_status,
} }
# Add Polyhaven handlers only if enabled # Add Polyhaven handlers only if enabled
@ -220,6 +222,14 @@ class BlenderMCPServer:
"import_generated_asset": self.import_generated_asset, "import_generated_asset": self.import_generated_asset,
} }
handlers.update(polyhaven_handlers) handlers.update(polyhaven_handlers)
# Add Sketchfab handlers only if enabled
if bpy.context.scene.blendermcp_use_sketchfab:
sketchfab_handlers = {
"search_sketchfab_models": self.search_sketchfab_models,
"download_sketchfab_model": self.download_sketchfab_model,
}
handlers.update(sketchfab_handlers)
handler = handlers.get(cmd_type) handler = handlers.get(cmd_type)
if handler: if handler:
@ -1373,6 +1383,248 @@ class BlenderMCPServer:
return {"succeed": False, "error": str(e)} return {"succeed": False, "error": str(e)}
#endregion #endregion
#region Sketchfab API
def get_sketchfab_status(self):
"""Get the current status of Sketchfab integration"""
enabled = bpy.context.scene.blendermcp_use_sketchfab
api_key = bpy.context.scene.blendermcp_sketchfab_api_key
# Test the API key if present
if api_key:
try:
headers = {
"Authorization": f"Token {api_key}"
}
response = requests.get(
"https://api.sketchfab.com/v3/me",
headers=headers
)
if response.status_code == 200:
user_data = response.json()
username = user_data.get("username", "Unknown user")
return {
"enabled": True,
"message": f"Sketchfab integration is enabled and ready to use. Logged in as: {username}"
}
else:
print(f"API key test failed with status code {response.status_code}: {response.text}")
return {
"enabled": False,
"message": f"Sketchfab API key seems invalid. Status code: {response.status_code}"
}
except Exception as e:
print(f"Error testing Sketchfab API key: {str(e)}")
return {
"enabled": False,
"message": f"Error testing Sketchfab API key: {str(e)}"
}
if enabled and api_key:
return {"enabled": True, "message": "Sketchfab integration is enabled and ready to use."}
elif enabled and not api_key:
return {
"enabled": False,
"message": """Sketchfab integration is currently enabled, but API key is not given. To enable it:
1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden)
2. Keep the 'Use Sketchfab' checkbox checked
3. Enter your Sketchfab API Key
4. Restart the connection to Claude"""
}
else:
return {
"enabled": False,
"message": """Sketchfab integration is currently disabled. To enable it:
1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden)
2. Check the 'Use assets from Sketchfab' checkbox
3. Enter your Sketchfab API Key
4. Restart the connection to Claude"""
}
def search_sketchfab_models(self, query, categories=None, count=20, downloadable=True):
"""Search for models on Sketchfab based on query and optional filters"""
try:
api_key = bpy.context.scene.blendermcp_sketchfab_api_key
if not api_key:
return {"error": "Sketchfab API key is not configured"}
print(f"Searching Sketchfab with query: {query}, categories: {categories}, count: {count}, downloadable: {downloadable}")
# Build search parameters with exact fields from Sketchfab API docs
params = {
"type": "models",
"q": query,
"count": count,
"downloadable": downloadable,
"archives_flavours": False
}
if categories:
params["categories"] = categories
# Make API request to Sketchfab search endpoint
# The proper format according to Sketchfab API docs for API key auth
headers = {
"Authorization": f"Token {api_key}"
}
print(f"Making request to Sketchfab API search endpoint with params: {params}")
# Use the search endpoint as specified in the API documentation
response = requests.get(
"https://api.sketchfab.com/v3/search",
headers=headers,
params=params
)
if response.status_code == 401:
print(f"Authentication failed with status code 401. Check your API key.")
return {"error": f"Authentication failed (401). Check your API key."}
if response.status_code != 200:
print(f"API request failed with status code {response.status_code}: {response.text}")
return {"error": f"API request failed with status code {response.status_code}: {response.text}"}
response_data = response.json()
print(f"API response received with status code {response.status_code}")
# Safety check on the response structure
if response_data is None:
return {"error": "Received empty response from Sketchfab API"}
# Handle 'results' potentially missing from response
results = response_data.get("results", [])
if not isinstance(results, list):
return {"error": f"Unexpected response format from Sketchfab API: {response_data}"}
return response_data
except json.JSONDecodeError as e:
print(f"JSON decoding error: {str(e)}. Response text: {response.text if 'response' in locals() else 'No response'}")
return {"error": f"Invalid JSON response from Sketchfab API: {str(e)}"}
except Exception as e:
print(f"Error in search_sketchfab_models: {str(e)}")
import traceback
traceback.print_exc()
return {"error": str(e)}
def download_sketchfab_model(self, uid):
"""Download a model from Sketchfab by its UID"""
try:
api_key = bpy.context.scene.blendermcp_sketchfab_api_key
if not api_key:
return {"error": "Sketchfab API key is not configured"}
print(f"Attempting to download Sketchfab model with UID: {uid}")
# Use proper authorization header for API key auth
headers = {
"Authorization": f"Token {api_key}"
}
print(f"Making download request to Sketchfab API with UID: {uid}")
# Request download URL using the exact endpoint from the documentation
download_endpoint = f"https://api.sketchfab.com/v3/models/{uid}/download"
print(f"Download endpoint: {download_endpoint}")
response = requests.get(
download_endpoint,
headers=headers
)
if response.status_code == 401:
print(f"Authentication failed with status code 401. Check your API key.")
return {"error": f"Authentication failed (401). Check your API key."}
if response.status_code != 200:
print(f"Download request failed with status code {response.status_code}: {response.text}")
return {"error": f"Download request failed with status code {response.status_code}: {response.text}"}
data = response.json()
# Safety check for None data
if data is None:
return {"error": "Received empty response from Sketchfab API for download request"}
print(f"Download response data: {data}")
# Extract download URL with safety checks
gltf_data = data.get("gltf")
if not gltf_data:
print(f"No gltf data in response: {data}")
return {"error": "No gltf download URL available for this model. Response: " + str(data)}
download_url = gltf_data.get("url")
if not download_url:
print(f"No URL in gltf data: {gltf_data}")
return {"error": "No download URL available for this model. Make sure the model is downloadable and you have access."}
print(f"Download URL obtained: {download_url}")
# Download the model
model_response = requests.get(download_url)
if model_response.status_code != 200:
print(f"Model download failed with status code {model_response.status_code}")
return {"error": f"Model download failed with status code {model_response.status_code}"}
# Save to temporary file
temp_dir = tempfile.mkdtemp()
file_path = os.path.join(temp_dir, f"{uid}.zip")
print(f"Saving downloaded model to temporary file: {file_path}")
with open(file_path, "wb") as f:
f.write(model_response.content)
# Extract the zip file
print(f"Extracting zip file to: {temp_dir}")
with zipfile.ZipFile(file_path, 'r') as zip_ref:
zip_ref.extractall(temp_dir)
# Find the main glTF file
gltf_files = [f for f in os.listdir(temp_dir) if f.endswith('.gltf') or f.endswith('.glb')]
if not gltf_files:
print(f"No glTF file found in the downloaded model. Directory contents: {os.listdir(temp_dir)}")
return {"error": "No glTF file found in the downloaded model"}
main_file = os.path.join(temp_dir, gltf_files[0])
print(f"Found main glTF file: {main_file}")
# Import the model
print(f"Importing model using Blender's glTF importer")
bpy.ops.import_scene.gltf(filepath=main_file)
# Get the names of imported objects
imported_objects = [obj.name for obj in bpy.context.selected_objects]
print(f"Imported objects: {', '.join(imported_objects)}")
# Clean up temporary files
try:
shutil.rmtree(temp_dir)
print(f"Temporary directory cleaned up: {temp_dir}")
except Exception as e:
print(f"Failed to clean up temporary directory: {temp_dir}. Error: {str(e)}")
return {
"success": True,
"message": f"Model imported successfully",
"imported_objects": imported_objects
}
except json.JSONDecodeError as e:
print(f"JSON decoding error: {str(e)}. Response text: {response.text if 'response' in locals() else 'No response'}")
return {"error": f"Invalid JSON response from Sketchfab API: {str(e)}"}
except Exception as e:
print(f"Error in download_sketchfab_model: {str(e)}")
import traceback
traceback.print_exc()
return {"error": f"Failed to download model: {str(e)}"}
#endregion
# Blender UI Panel # Blender UI Panel
class BLENDERMCP_PT_Panel(bpy.types.Panel): class BLENDERMCP_PT_Panel(bpy.types.Panel):
bl_label = "Blender MCP" bl_label = "Blender MCP"
@ -1394,6 +1646,10 @@ class BLENDERMCP_PT_Panel(bpy.types.Panel):
layout.prop(scene, "blendermcp_hyper3d_api_key", text="API Key") layout.prop(scene, "blendermcp_hyper3d_api_key", text="API Key")
layout.operator("blendermcp.set_hyper3d_free_trial_api_key", text="Set Free Trial API Key") layout.operator("blendermcp.set_hyper3d_free_trial_api_key", text="Set Free Trial API Key")
layout.prop(scene, "blendermcp_use_sketchfab", text="Use assets from Sketchfab")
if scene.blendermcp_use_sketchfab:
layout.prop(scene, "blendermcp_sketchfab_api_key", text="API Key")
if not scene.blendermcp_server_running: if not scene.blendermcp_server_running:
layout.operator("blendermcp.start_server", text="Connect to MCP server") layout.operator("blendermcp.start_server", text="Connect to MCP server")
else: else:
@ -1492,6 +1748,19 @@ def register():
default="" default=""
) )
bpy.types.Scene.blendermcp_use_sketchfab = bpy.props.BoolProperty(
name="Use Sketchfab",
description="Enable Sketchfab asset integration",
default=False
)
bpy.types.Scene.blendermcp_sketchfab_api_key = bpy.props.StringProperty(
name="Sketchfab API Key",
subtype="PASSWORD",
description="API Key provided by Sketchfab",
default=""
)
bpy.utils.register_class(BLENDERMCP_PT_Panel) bpy.utils.register_class(BLENDERMCP_PT_Panel)
bpy.utils.register_class(BLENDERMCP_OT_SetFreeTrialHyper3DAPIKey) bpy.utils.register_class(BLENDERMCP_OT_SetFreeTrialHyper3DAPIKey)
bpy.utils.register_class(BLENDERMCP_OT_StartServer) bpy.utils.register_class(BLENDERMCP_OT_StartServer)
@ -1516,6 +1785,8 @@ def unregister():
del bpy.types.Scene.blendermcp_use_hyper3d del bpy.types.Scene.blendermcp_use_hyper3d
del bpy.types.Scene.blendermcp_hyper3d_mode del bpy.types.Scene.blendermcp_hyper3d_mode
del bpy.types.Scene.blendermcp_hyper3d_api_key del bpy.types.Scene.blendermcp_hyper3d_api_key
del bpy.types.Scene.blendermcp_use_sketchfab
del bpy.types.Scene.blendermcp_sketchfab_api_key
print("BlenderMCP addon unregistered") print("BlenderMCP addon unregistered")

View File

@ -515,6 +515,144 @@ def get_hyper3d_status(ctx: Context) -> str:
logger.error(f"Error checking Hyper3D status: {str(e)}") logger.error(f"Error checking Hyper3D status: {str(e)}")
return f"Error checking Hyper3D status: {str(e)}" return f"Error checking Hyper3D status: {str(e)}"
@mcp.tool()
def get_sketchfab_status(ctx: Context) -> str:
"""
Check if Sketchfab integration is enabled in Blender.
Returns a message indicating whether Sketchfab features are available.
"""
try:
blender = get_blender_connection()
result = blender.send_command("get_sketchfab_status")
enabled = result.get("enabled", False)
message = result.get("message", "")
return message
except Exception as e:
logger.error(f"Error checking Sketchfab status: {str(e)}")
return f"Error checking Sketchfab status: {str(e)}"
@mcp.tool()
def search_sketchfab_models(
ctx: Context,
query: str,
categories: str = None,
count: int = 20,
downloadable: bool = True
) -> str:
"""
Search for models on Sketchfab with optional filtering.
Parameters:
- query: Text to search for
- categories: Optional comma-separated list of categories
- count: Maximum number of results to return (default 20)
- downloadable: Whether to include only downloadable models (default True)
Returns a formatted list of matching models.
"""
try:
blender = get_blender_connection()
logger.info(f"Searching Sketchfab models with query: {query}, categories: {categories}, count: {count}, downloadable: {downloadable}")
result = blender.send_command("search_sketchfab_models", {
"query": query,
"categories": categories,
"count": count,
"downloadable": downloadable
})
if "error" in result:
logger.error(f"Error from Sketchfab search: {result['error']}")
return f"Error: {result['error']}"
# Safely get results with fallbacks for None
if result is None:
logger.error("Received None result from Sketchfab search")
return "Error: Received no response from Sketchfab search"
# Format the results
models = result.get("results", []) or []
if not models:
return f"No models found matching '{query}'"
formatted_output = f"Found {len(models)} models matching '{query}':\n\n"
for model in models:
if model is None:
continue
model_name = model.get("name", "Unnamed model")
model_uid = model.get("uid", "Unknown ID")
formatted_output += f"- {model_name} (UID: {model_uid})\n"
# Get user info with safety checks
user = model.get("user") or {}
username = user.get("username", "Unknown author") if isinstance(user, dict) else "Unknown author"
formatted_output += f" Author: {username}\n"
# Get license info with safety checks
license_data = model.get("license") or {}
license_label = license_data.get("label", "Unknown") if isinstance(license_data, dict) else "Unknown"
formatted_output += f" License: {license_label}\n"
# Add face count and downloadable status
face_count = model.get("faceCount", "Unknown")
is_downloadable = "Yes" if model.get("isDownloadable") else "No"
formatted_output += f" Face count: {face_count}\n"
formatted_output += f" Downloadable: {is_downloadable}\n\n"
return formatted_output
except Exception as e:
logger.error(f"Error searching Sketchfab models: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return f"Error searching Sketchfab models: {str(e)}"
@mcp.tool()
def download_sketchfab_model(
ctx: Context,
uid: str
) -> str:
"""
Download and import a Sketchfab model by its UID.
Parameters:
- uid: The unique identifier of the Sketchfab model
Returns a message indicating success or failure.
The model must be downloadable and you must have proper access rights.
"""
try:
blender = get_blender_connection()
logger.info(f"Attempting to download Sketchfab model with UID: {uid}")
result = blender.send_command("download_sketchfab_model", {
"uid": uid
})
if result is None:
logger.error("Received None result from Sketchfab download")
return "Error: Received no response from Sketchfab download request"
if "error" in result:
logger.error(f"Error from Sketchfab download: {result['error']}")
return f"Error: {result['error']}"
if result.get("success"):
imported_objects = result.get("imported_objects", [])
object_names = ", ".join(imported_objects) if imported_objects else "none"
return f"Successfully imported model. Created objects: {object_names}"
else:
return f"Failed to download model: {result.get('message', 'Unknown error')}"
except Exception as e:
logger.error(f"Error downloading Sketchfab model: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return f"Error downloading Sketchfab model: {str(e)}"
def _process_bbox(original_bbox: list[float] | list[int] | None) -> list[int] | None: def _process_bbox(original_bbox: list[float] | list[int] | None) -> list[int] | None:
if original_bbox is None: if original_bbox is None:
return None return None
@ -705,7 +843,14 @@ def asset_creation_strategy() -> str:
- For objects/models: Use download_polyhaven_asset() with asset_type="models" - For objects/models: Use download_polyhaven_asset() with asset_type="models"
- For materials/textures: Use download_polyhaven_asset() with asset_type="textures" - For materials/textures: Use download_polyhaven_asset() with asset_type="textures"
- For environment lighting: Use download_polyhaven_asset() with asset_type="hdris" - For environment lighting: Use download_polyhaven_asset() with asset_type="hdris"
2. Hyper3D(Rodin) 2. Sketchfab
Use get_sketchfab_status() to verify its status
If Sketchfab is enabled:
- For objects/models: First search using search_sketchfab_models() with your query
- Then download specific models using download_sketchfab_model() with the UID
- Note that only downloadable models can be accessed, and API key must be properly configured
- Sketchfab has a wider variety of models than PolyHaven, especially for specific subjects
3. Hyper3D(Rodin)
Hyper3D Rodin is good at generating 3D models for single item. Hyper3D Rodin is good at generating 3D models for single item.
So don't try to: So don't try to:
1. Generate the whole scene with one shot 1. Generate the whole scene with one shot
@ -735,11 +880,17 @@ def asset_creation_strategy() -> str:
- Ensure that all objects that should not be clipping are not clipping. - Ensure that all objects that should not be clipping are not clipping.
- Items have right spatial relationship. - Items have right spatial relationship.
4. Recommended asset source priority:
- For specific existing objects: First try Sketchfab, then PolyHaven
- For generic objects/furniture: First try PolyHaven, then Sketchfab
- For custom or unique items not available in libraries: Use Hyper3D Rodin
- For environment lighting: Use PolyHaven HDRIs
- For materials/textures: Use PolyHaven textures
Only fall back to scripting when: Only fall back to scripting when:
- PolyHaven and Hyper3D are disabled - PolyHaven, Sketchfab, and Hyper3D are all disabled
- A simple primitive is explicitly requested - A simple primitive is explicitly requested
- No suitable PolyHaven asset exists - No suitable asset exists in any of the libraries
- Hyper3D Rodin failed to generate the desired asset - Hyper3D Rodin failed to generate the desired asset
- The task specifically requires a basic material/color - The task specifically requires a basic material/color
""" """