connecting but request timeout in the socket

This commit is contained in:
ahujasid 2025-03-07 15:29:16 +05:30
commit 4372edf2a7
10 changed files with 2584 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.13.2

499
MCP.md Normal file
View File

@ -0,0 +1,499 @@
# MCP Python SDK
## Overview
The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This Python SDK implements the full MCP specification, making it easy to:
- Build MCP clients that can connect to any MCP server
- Create MCP servers that expose resources, prompts and tools
- Use standard transports like stdio and SSE
- Handle all MCP protocol messages and lifecycle events
## Installation
We recommend using [uv](https://docs.astral.sh/uv/) to manage your Python projects:
```bash
uv add "mcp[cli]"
```
Alternatively:
```bash
pip install mcp
```
## Quickstart
Let's create a simple MCP server that exposes a calculator tool and some data:
```python
# server.py
from mcp.server.fastmcp import FastMCP
# Create an MCP server
mcp = FastMCP("Demo")
# Add an addition tool
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
# Add a dynamic greeting resource
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
"""Get a personalized greeting"""
return f"Hello, {name}!"
```
You can install this server in [Claude Desktop](https://claude.ai/download) and interact with it right away by running:
```bash
mcp install server.py
```
Alternatively, you can test it with the MCP Inspector:
```bash
mcp dev server.py
```
## What is MCP?
The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can:
- Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context)
- Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect)
- Define interaction patterns through **Prompts** (reusable templates for LLM interactions)
- And more!
## Core Concepts
### Server
The FastMCP server is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing:
```python
# Add lifespan support for startup/shutdown with strong typing
from dataclasses import dataclass
from typing import AsyncIterator
from mcp.server.fastmcp import FastMCP
# Create a named server
mcp = FastMCP("My App")
# Specify dependencies for deployment and development
mcp = FastMCP("My App", dependencies=["pandas", "numpy"])
@dataclass
class AppContext:
db: Database # Replace with your actual DB type
@asynccontextmanager
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
"""Manage application lifecycle with type-safe context"""
try:
# Initialize on startup
await db.connect()
yield AppContext(db=db)
finally:
# Cleanup on shutdown
await db.disconnect()
# Pass lifespan to server
mcp = FastMCP("My App", lifespan=app_lifespan)
# Access type-safe lifespan context in tools
@mcp.tool()
def query_db(ctx: Context) -> str:
"""Tool that uses initialized resources"""
db = ctx.request_context.lifespan_context["db"]
return db.query()
```
### Resources
Resources are how you expose data to LLMs. They're similar to GET endpoints in a REST API - they provide data but shouldn't perform significant computation or have side effects:
```python
@mcp.resource("config://app")
def get_config() -> str:
"""Static configuration data"""
return "App configuration here"
@mcp.resource("users://{user_id}/profile")
def get_user_profile(user_id: str) -> str:
"""Dynamic user data"""
return f"Profile data for user {user_id}"
```
### Tools
Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects:
```python
@mcp.tool()
def calculate_bmi(weight_kg: float, height_m: float) -> float:
"""Calculate BMI given weight in kg and height in meters"""
return weight_kg / (height_m ** 2)
@mcp.tool()
async def fetch_weather(city: str) -> str:
"""Fetch current weather for a city"""
async with httpx.AsyncClient() as client:
response = await client.get(f"https://api.weather.com/{city}")
return response.text
```
### Prompts
Prompts are reusable templates that help LLMs interact with your server effectively:
```python
@mcp.prompt()
def review_code(code: str) -> str:
return f"Please review this code:\n\n{code}"
@mcp.prompt()
def debug_error(error: str) -> list[Message]:
return [
UserMessage("I'm seeing this error:"),
UserMessage(error),
AssistantMessage("I'll help debug that. What have you tried so far?")
]
```
### Images
FastMCP provides an `Image` class that automatically handles image data:
```python
from mcp.server.fastmcp import FastMCP, Image
from PIL import Image as PILImage
@mcp.tool()
def create_thumbnail(image_path: str) -> Image:
"""Create a thumbnail from an image"""
img = PILImage.open(image_path)
img.thumbnail((100, 100))
return Image(data=img.tobytes(), format="png")
```
### Context
The Context object gives your tools and resources access to MCP capabilities:
```python
from mcp.server.fastmcp import FastMCP, Context
@mcp.tool()
async def long_task(files: list[str], ctx: Context) -> str:
"""Process multiple files with progress tracking"""
for i, file in enumerate(files):
ctx.info(f"Processing {file}")
await ctx.report_progress(i, len(files))
data, mime_type = await ctx.read_resource(f"file://{file}")
return "Processing complete"
```
## Running Your Server
### Development Mode
The fastest way to test and debug your server is with the MCP Inspector:
```bash
mcp dev server.py
# Add dependencies
mcp dev server.py --with pandas --with numpy
# Mount local code
mcp dev server.py --with-editable .
```
### Claude Desktop Integration
Once your server is ready, install it in Claude Desktop:
```bash
mcp install server.py
# Custom name
mcp install server.py --name "My Analytics Server"
# Environment variables
mcp install server.py -v API_KEY=abc123 -v DB_URL=postgres://...
mcp install server.py -f .env
```
### Direct Execution
For advanced scenarios like custom deployments:
```python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("My App")
if __name__ == "__main__":
mcp.run()
```
Run it with:
```bash
python server.py
# or
mcp run server.py
```
## Examples
### Echo Server
A simple server demonstrating resources, tools, and prompts:
```python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Echo")
@mcp.resource("echo://{message}")
def echo_resource(message: str) -> str:
"""Echo a message as a resource"""
return f"Resource echo: {message}"
@mcp.tool()
def echo_tool(message: str) -> str:
"""Echo a message as a tool"""
return f"Tool echo: {message}"
@mcp.prompt()
def echo_prompt(message: str) -> str:
"""Create an echo prompt"""
return f"Please process this message: {message}"
```
### SQLite Explorer
A more complex example showing database integration:
```python
from mcp.server.fastmcp import FastMCP
import sqlite3
mcp = FastMCP("SQLite Explorer")
@mcp.resource("schema://main")
def get_schema() -> str:
"""Provide the database schema as a resource"""
conn = sqlite3.connect("database.db")
schema = conn.execute(
"SELECT sql FROM sqlite_master WHERE type='table'"
).fetchall()
return "\n".join(sql[0] for sql in schema if sql[0])
@mcp.tool()
def query_data(sql: str) -> str:
"""Execute SQL queries safely"""
conn = sqlite3.connect("database.db")
try:
result = conn.execute(sql).fetchall()
return "\n".join(str(row) for row in result)
except Exception as e:
return f"Error: {str(e)}"
```
## Advanced Usage
### Low-Level Server
For more control, you can use the low-level server implementation directly. This gives you full access to the protocol and allows you to customize every aspect of your server, including lifecycle management through the lifespan API:
```python
from contextlib import asynccontextmanager
from typing import AsyncIterator
@asynccontextmanager
async def server_lifespan(server: Server) -> AsyncIterator[dict]:
"""Manage server startup and shutdown lifecycle."""
try:
# Initialize resources on startup
await db.connect()
yield {"db": db}
finally:
# Clean up on shutdown
await db.disconnect()
# Pass lifespan to server
server = Server("example-server", lifespan=server_lifespan)
# Access lifespan context in handlers
@server.call_tool()
async def query_db(name: str, arguments: dict) -> list:
ctx = server.request_context
db = ctx.lifespan_context["db"]
return await db.query(arguments["query"])
```
The lifespan API provides:
- A way to initialize resources when the server starts and clean them up when it stops
- Access to initialized resources through the request context in handlers
- Type-safe context passing between lifespan and request handlers
```python
from mcp.server.lowlevel import Server, NotificationOptions
from mcp.server.models import InitializationOptions
import mcp.server.stdio
import mcp.types as types
# Create a server instance
server = Server("example-server")
@server.list_prompts()
async def handle_list_prompts() -> list[types.Prompt]:
return [
types.Prompt(
name="example-prompt",
description="An example prompt template",
arguments=[
types.PromptArgument(
name="arg1",
description="Example argument",
required=True
)
]
)
]
@server.get_prompt()
async def handle_get_prompt(
name: str,
arguments: dict[str, str] | None
) -> types.GetPromptResult:
if name != "example-prompt":
raise ValueError(f"Unknown prompt: {name}")
return types.GetPromptResult(
description="Example prompt",
messages=[
types.PromptMessage(
role="user",
content=types.TextContent(
type="text",
text="Example prompt text"
)
)
]
)
async def run():
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="example",
server_version="0.1.0",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
)
)
)
if __name__ == "__main__":
import asyncio
asyncio.run(run())
```
### Writing MCP Clients
The SDK provides a high-level client interface for connecting to MCP servers:
```python
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
# Create server parameters for stdio connection
server_params = StdioServerParameters(
command="python", # Executable
args=["example_server.py"], # Optional command line arguments
env=None # Optional environment variables
)
# Optional: create a sampling callback
async def handle_sampling_message(message: types.CreateMessageRequestParams) -> types.CreateMessageResult:
return types.CreateMessageResult(
role="assistant",
content=types.TextContent(
type="text",
text="Hello, world! from model",
),
model="gpt-3.5-turbo",
stopReason="endTurn",
)
async def run():
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write, sampling_callback=handle_sampling_message) as session:
# Initialize the connection
await session.initialize()
# List available prompts
prompts = await session.list_prompts()
# Get a prompt
prompt = await session.get_prompt("example-prompt", arguments={"arg1": "value"})
# List available resources
resources = await session.list_resources()
# List available tools
tools = await session.list_tools()
# Read a resource
content, mime_type = await session.read_resource("file://some/path")
# Call a tool
result = await session.call_tool("tool-name", arguments={"arg1": "value"})
if __name__ == "__main__":
import asyncio
asyncio.run(run())
```
### MCP Primitives
The MCP protocol defines three core primitives that servers can implement:
| Primitive | Control | Description | Example Use |
|-----------|-----------------------|-----------------------------------------------------|------------------------------|
| Prompts | User-controlled | Interactive templates invoked by user choice | Slash commands, menu options |
| Resources | Application-controlled| Contextual data managed by the client application | File contents, API responses |
| Tools | Model-controlled | Functions exposed to the LLM to take actions | API calls, data updates |
### Server Capabilities
MCP servers declare capabilities during initialization:
| Capability | Feature Flag | Description |
|-------------|------------------------------|------------------------------------|
| `prompts` | `listChanged` | Prompt template management |
| `resources` | `subscribe`<br/>`listChanged`| Resource exposure and updates |
| `tools` | `listChanged` | Tool discovery and execution |
| `logging` | - | Server logging configuration |
| `completion`| - | Argument completion suggestions |
## Documentation
- [Model Context Protocol documentation](https://modelcontextprotocol.io)
- [Model Context Protocol specification](https://spec.modelcontextprotocol.io)
- [Officially supported servers](https://github.com/modelcontextprotocol/servers)
## Contributing
We are passionate about supporting contributors of all levels of experience and would love to see you get involved in the project. See the [contributing guide](CONTRIBUTING.md) to get started.
## License
This project is licensed under the MIT License - see the LICENSE file for details.

517
README.md Normal file
View File

@ -0,0 +1,517 @@
# BlenderMCP Setup Guide
This guide will walk you through setting up BlenderMCP, which allows Claude to communicate with Blender through the Model Context Protocol (MCP).
## Prerequisites
- Blender 3.0 or newer
- Python 3.9 or newer
- Claude Desktop or Cursor
- Basic familiarity with Blender and Python
## Installation Steps
### 1. Install the BlenderMCP Addon in Blender
1. Save the `blender_addon.py` file to your computer
2. Open Blender
3. Go to Edit > Preferences > Add-ons
4. Click "Install..." and select the `blender_addon.py` file
5. Enable the addon by checking the box next to "Interface: Blender MCP"
### 2. Set Up the MCP Server
1. Install the MCP Python package:
```bash
pip install "mcp[cli]"
```
2. Save the `blender_mcp_server.py` file to your computer
3. Install the server in Claude Desktop:
```bash
mcp install blender_mcp_server.py
```
Alternatively, you can run the server in development mode:
```bash
mcp dev blender_mcp_server.py
```
### 3. Start the Blender Server
1. In Blender, go to the 3D View
2. Open the sidebar (press N if it's not visible)
3. Select the "BlenderMCP" tab
4. Click "Start MCP Server" (default port is 9876)
### 4. Connect Claude to Blender
1. Open Claude Desktop
2. The BlenderMCP server should now be available in Claude's MCP connections
3. Start a conversation with Claude and you can now control Blender using natural language!
## Usage Examples
Here are some examples of commands you can give Claude to control Blender:
### Basic Scene Creation
```
Create a simple scene with a cube, sphere, and a ground plane.
```
### Manipulating Objects
```
Move the cube 2 units up along the Z-axis.
```
```
Scale the sphere to be twice its current size.
```
```
Rotate the cylinder 45 degrees around the Y-axis.
```
### Material Operations
```
Create a red glossy material and apply it to the cube.
```
```
Make the sphere blue with some metallic properties.
```
### Rendering
```
Render the current scene and save it to my desktop.
```
```
Change the render resolution to 1920x1080 and render the scene.
```
## Troubleshooting
### MCP Server Connection Issues
If Claude can't connect to the MCP server:
1. Make sure the Blender addon is running (check the BlenderMCP panel in Blender)
2. Verify the port number in Blender matches what the MCP server is expecting (default: 9876)
3. Restart the MCP server in Claude Desktop
4. Restart Blender and try again
### Addon Issues
If the Blender addon isn't working:
1. Check the Blender system console for error messages
2. Verify that the addon is enabled in Blender's preferences
3. Try reinstalling the addon
## Advanced Configuration
### Customizing the Port
You can change the default port (9876) in the BlenderMCP panel in Blender. If you change this port, you'll need to update the `BlenderConnection` in the MCP server code accordingly.
### Adding New Commands
The system is designed to be extensible. You can add new commands by:
1. Adding new methods to the `BlenderMCPServer` class in the Blender addon
2. Adding corresponding tools or resources to the MCP server
## Security Considerations
- The `execute_blender_code` tool allows arbitrary code execution within Blender. Use with caution!
- The socket connection between the MCP server and Blender is not encrypted or authenticated.
- Always review the commands Claude suggests before allowing them to execute in Blender.
---
# BlenderMCP Usage Examples
These examples demonstrate the natural language capabilities and show how Claude can understand scene context and provide intelligent assistance.
## Basic Scene Creation
### Example 1: Creating a Simple Scene
**User**: "Create a basic scene with a cube sitting on a plane."
**Claude's Understanding and Actions**:
1. Recognizes need for a ground plane and a cube object
2. Creates a plane at origin with appropriate scale
3. Creates a cube positioned above the plane
4. Sets basic materials for visibility
5. Ensures camera and lighting are properly positioned
**MCP Tools Used**:
- `create_object` tool for the plane and cube
- `set_material` tool for materials
- `modify_object` tool for positioning
### Example 2: Building a Table
**User**: "I want to create a simple table with four legs."
**Claude's Understanding and Actions**:
1. Creates a flattened cube for the tabletop
2. Creates four cylinders for table legs
3. Positions legs at the corners of the tabletop
4. Applies appropriate materials
5. Reports back the created objects and their relationships
**Implementation via BlenderMCP**:
```python
# Create tabletop
tabletop = blender.send_command("create_object", {
"type": "CUBE",
"name": "Tabletop",
"location": [0, 0, 1],
"scale": [2, 1, 0.1]
})
# Create and position legs
leg_positions = [
[1.8, 0.8, 0.5], # front-right
[1.8, -0.8, 0.5], # back-right
[-1.8, 0.8, 0.5], # front-left
[-1.8, -0.8, 0.5] # back-left
]
for i, pos in enumerate(leg_positions):
leg = blender.send_command("create_object", {
"type": "CYLINDER",
"name": f"Leg_{i+1}",
"location": pos,
"scale": [0.1, 0.1, 0.5]
})
```
## Object Manipulation
### Example 3: Moving and Scaling Objects
**User**: "Make the cube twice as big and move it 3 units higher."
**Claude's Understanding and Actions**:
1. Identifies the cube in the scene
2. Uses `modify_object` tool to scale it by a factor of 2
3. Adjusts the z-coordinate to move it upward
4. Reports the new position and scale
### Example 4: Complex Transformation
**User**: "Arrange five spheres in a circular pattern around the origin."
**Claude's Understanding and Actions**:
1. Calculates positions in a circular pattern using trigonometry
2. Creates five spheres with appropriate positioning
3. Possibly applies different materials for visibility
**Implementation via BlenderMCP**:
```python
import math
# Calculate positions in a circle
radius = 3
num_spheres = 5
for i in range(num_spheres):
angle = 2 * math.pi * i / num_spheres
x = radius * math.cos(angle)
y = radius * math.sin(angle)
# Create sphere at calculated position
sphere = blender.send_command("create_object", {
"type": "SPHERE",
"name": f"Sphere_{i+1}",
"location": [x, y, 1],
"scale": [0.5, 0.5, 0.5]
})
# Apply different colors
hue = i / num_spheres
r, g, b = hsv_to_rgb(hue, 0.8, 0.8)
blender.send_command("set_material", {
"object_name": sphere["name"],
"color": [r, g, b]
})
```
## Material and Appearance
### Example 5: Creating and Applying Materials
**User**: "Create a glossy red material and apply it to the cube."
**Claude's Understanding and Actions**:
1. Uses `set_material` tool with appropriate color values
2. May use `execute_blender_code` for more advanced material settings
3. Reports back the material creation and assignment
### Example 6: Scene Lighting
**User**: "Make the scene brighter with a three-point lighting setup."
**Claude's Understanding and Actions**:
1. Creates three lights (key, fill, and back)
2. Positions them appropriately for three-point lighting
3. Sets different intensities for each light
4. May adjust the existing environment lighting
**Implementation via BlenderMCP**:
```python
# Create key light (main light)
key_light = blender.send_command("create_object", {
"type": "LIGHT",
"name": "Key_Light",
"location": [4, -4, 5]
})
# Create fill light (softer, fills shadows)
fill_light = blender.send_command("create_object", {
"type": "LIGHT",
"name": "Fill_Light",
"location": [-4, -2, 3]
})
# Create back light (rim light)
back_light = blender.send_command("create_object", {
"type": "LIGHT",
"name": "Back_Light",
"location": [0, 5, 4]
})
# Set light properties using execute_code
blender.send_command("execute_code", {
"code": """
import bpy
# Set key light properties
key = bpy.data.objects['Key_Light']
key.data.energy = 1000
key.data.type = 'AREA'
# Set fill light properties
fill = bpy.data.objects['Fill_Light']
fill.data.energy = 400
fill.data.type = 'AREA'
# Set back light properties
back = bpy.data.objects['Back_Light']
back.data.energy = 600
back.data.type = 'AREA'
"""
})
```
## Advanced Scene Creation
### Example 7: Architectural Elements
**User**: "Create a simple house with walls, a roof, and windows."
**Claude's Understanding and Actions**:
1. Creates multiple objects to form the structure
2. Positions them according to architectural understanding
3. Uses appropriate materials for different elements
4. Reports the structure creation with a breakdown of elements
### Example 8: Terrain Generation
**User**: "Can you create a simple mountain landscape?"
**Claude's Understanding and Actions**:
1. Creates a plane as the base terrain
2. Uses `execute_blender_code` to add displacement or sculpting
3. Applies appropriate textures and materials
4. Sets up environment lighting for landscape visualization
**Implementation via BlenderMCP**:
```python
# Create a plane for the terrain
terrain = blender.send_command("create_object", {
"type": "PLANE",
"name": "Terrain",
"location": [0, 0, 0],
"scale": [10, 10, 1]
})
# Use Python code to add subdivisions and displacement
blender.send_command("execute_code", {
"code": """
import bpy
import random
# Get the terrain object
terrain = bpy.data.objects['Terrain']
# Add subdivision modifier
subsurf = terrain.modifiers.new(name="Subdivision", type='SUBSURF')
subsurf.levels = 5
subsurf.render_levels = 5
# Add displacement modifier
displace = terrain.modifiers.new(name="Displace", type='DISPLACE')
# Create a new texture for displacement
tex = bpy.data.textures.new('Mountain', type='CLOUDS')
tex.noise_scale = 1.5
displace.texture = tex
displace.strength = 2.0
# Apply modifiers
bpy.context.view_layer.objects.active = terrain
bpy.ops.object.modifier_apply(modifier="Subdivision")
bpy.ops.object.modifier_apply(modifier="Displace")
# Set material
if 'Terrain_Mat' not in bpy.data.materials:
mat = bpy.data.materials.new(name="Terrain_Mat")
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get('Principled BSDF')
bsdf.inputs[0].default_value = (0.08, 0.258, 0.12, 1.0) # Green color
terrain.data.materials.append(mat)
"""
})
```
## Animation and Rendering
### Example 9: Simple Animation
**User**: "Create a 30-frame animation of the cube rotating around the Y-axis."
**Claude's Understanding and Actions**:
1. Sets up keyframes for the cube's rotation
2. Configures animation settings
3. Reports back the animation setup and how to play it
**Implementation via BlenderMCP**:
```python
# Use execute_code to set up animation
blender.send_command("execute_code", {
"code": """
import bpy
import math
# Get the cube
cube = bpy.data.objects.get('Cube')
if not cube:
# Create a cube if it doesn't exist
bpy.ops.mesh.primitive_cube_add(location=(0, 0, 1))
cube = bpy.context.active_object
cube.name = 'Cube'
# Set animation length
scene = bpy.context.scene
scene.frame_start = 1
scene.frame_end = 30
# Set first keyframe at frame 1
scene.frame_set(1)
cube.rotation_euler = (0, 0, 0)
cube.keyframe_insert(data_path="rotation_euler")
# Set final keyframe at frame 30
scene.frame_set(30)
cube.rotation_euler = (0, 2*math.pi, 0) # Full 360-degree rotation
cube.keyframe_insert(data_path="rotation_euler")
"""
})
```
### Example 10: Rendering and Output
**User**: "Render the scene with a resolution of 1920x1080 and save it to my desktop."
**Claude's Understanding and Actions**:
1. Uses `render_scene` tool with appropriate parameters
2. Sets resolution and output path
3. Initiates rendering process
4. Reports back when rendering is complete
## Debugging and Scene Analysis
### Example 11: Scene Inspection
**User**: "What objects are currently in my scene?"
**Claude's Understanding and Actions**:
1. Uses `get_scene_info` resource to fetch scene data
2. Analyzes the object list
3. Provides a formatted summary of all objects, their types, and positions
### Example 12: Troubleshooting
**User**: "Why can't I see the sphere in the render?"
**Claude's Understanding and Actions**:
1. Uses resources to check scene and object information
2. Checks visibility settings, materials, and camera position
3. Provides diagnostic information and suggested fixes
**Potential Fixes via BlenderMCP**:
```python
# Check sphere visibility and material
sphere_info = blender.send_command("get_object_info", {"name": "Sphere"})
# Make sure it's visible
if not sphere_info.get("visible", True):
blender.send_command("modify_object", {
"name": "Sphere",
"visible": True
})
# Check if it has a material, add one if missing
if not sphere_info.get("materials"):
blender.send_command("set_material", {
"object_name": "Sphere",
"color": [0.2, 0.4, 0.8]
})
# Check if it's in camera view
blender.send_command("execute_code", {
"code": """
import bpy
# Select sphere
sphere = bpy.data.objects.get('Sphere')
if sphere:
bpy.ops.object.select_all(action='DESELECT')
sphere.select_set(True)
# Frame selected in camera view
bpy.ops.view3d.camera_to_view_selected()
"""
})
```
## Real-Time Collaboration
### Example 13: Iterative Design
**User**: "I don't like how the table looks. Make the legs thicker and the tabletop wider."
**Claude's Understanding and Actions**:
1. Identifies existing objects (tabletop and legs)
2. Modifies their properties based on the feedback
3. Shows before/after comparison and asks for further feedback
### Example 14: Guided Tutorial
**User**: "I'm new to Blender. Can you help me create a simple character?"
**Claude's Understanding and Actions**:
1. Breaks down the process into manageable steps
2. Creates basic shapes for the character parts
3. Guides through joining and posing
4. Provides educational context along with actions
These examples showcase how Claude can interact with Blender through natural language using the BlenderMCP integration, providing an intuitive way to create and modify 3D content without needing to know the details of Blender's interface or Python API.

511
blender_mcp_addon.py Normal file
View File

@ -0,0 +1,511 @@
import bpy
import json
import threading
import socket
import time
from bpy.props import StringProperty, IntProperty
bl_info = {
"name": "Blender MCP",
"author": "BlenderMCP",
"version": (0, 1),
"blender": (3, 0, 0),
"location": "View3D > Sidebar > BlenderMCP",
"description": "Connect Blender to Claude via MCP",
"category": "Interface",
}
class BlenderMCPServer:
def __init__(self, host='localhost', port=9876):
self.host = host
self.port = port
self.running = False
self.socket = None
self.client = None
self.server_thread = None
def start(self):
self.running = True
self.server_thread = threading.Thread(target=self._run_server)
self.server_thread.daemon = True
self.server_thread.start()
print(f"BlenderMCP server started on {self.host}:{self.port}")
def stop(self):
self.running = False
if self.socket:
self.socket.close()
if self.client:
self.client.close()
print("BlenderMCP server stopped")
def _run_server(self):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
self.socket.bind((self.host, self.port))
self.socket.listen(1)
self.socket.settimeout(1.0) # Add a timeout for accept
while self.running:
try:
self.client, address = self.socket.accept()
print(f"Connected to client: {address}")
while self.running:
try:
# Set a timeout for receiving data
self.client.settimeout(15.0)
data = self.client.recv(4096)
if not data:
print("Empty data received, client may have disconnected")
break
try:
print(f"Received data: {data.decode('utf-8')}")
command = json.loads(data.decode('utf-8'))
response = self.execute_command(command)
print(f"Sending response: {json.dumps(response)[:100]}...") # Truncate long responses in log
self.client.sendall(json.dumps(response).encode('utf-8'))
except json.JSONDecodeError:
print(f"Invalid JSON received: {data.decode('utf-8')}")
self.client.sendall(json.dumps({
"status": "error",
"message": "Invalid JSON format"
}).encode('utf-8'))
except Exception as e:
print(f"Error executing command: {str(e)}")
import traceback
traceback.print_exc()
self.client.sendall(json.dumps({
"status": "error",
"message": str(e)
}).encode('utf-8'))
except socket.timeout:
print("Socket timeout while waiting for data")
continue
except Exception as e:
print(f"Error receiving data: {str(e)}")
break
self.client.close()
self.client = None
except socket.timeout:
# This is normal - just continue the loop
continue
except Exception as e:
print(f"Connection error: {str(e)}")
if self.client:
self.client.close()
self.client = None
time.sleep(1) # Prevent busy waiting
except Exception as e:
print(f"Server error: {str(e)}")
finally:
if self.socket:
self.socket.close()
def execute_command(self, command):
"""Execute a Blender command received from the MCP server"""
cmd_type = command.get("type")
params = command.get("params", {})
# Add a simple ping handler
if cmd_type == "ping":
print("Handling ping command")
return {"status": "success", "result": {"pong": True}}
handlers = {
"ping": lambda **kwargs: {"pong": True},
"get_simple_info": self.get_simple_info,
"get_scene_info": self.get_scene_info,
"create_object": self.create_object,
"modify_object": self.modify_object,
"delete_object": self.delete_object,
"get_object_info": self.get_object_info,
"execute_code": self.execute_code,
"set_material": self.set_material,
"render_scene": self.render_scene,
}
handler = handlers.get(cmd_type)
if handler:
try:
result = handler(**params)
return {"status": "success", "result": result}
except Exception as e:
return {"status": "error", "message": str(e)}
else:
return {"status": "error", "message": f"Unknown command type: {cmd_type}"}
def get_simple_info(self):
"""Get basic Blender information"""
return {
"blender_version": ".".join(str(v) for v in bpy.app.version),
"scene_name": bpy.context.scene.name,
"object_count": len(bpy.context.scene.objects)
}
def get_scene_info(self):
"""Get information about the current Blender scene"""
try:
print("Getting scene info...")
scene_info = {
"objects": [],
"materials": [],
"camera": {},
"render_settings": {},
}
# Collect object information (limit to first 20 objects to prevent oversized responses)
for i, obj in enumerate(bpy.context.scene.objects):
if i >= 20: # Limit to 20 objects to prevent oversized responses
break
obj_info = {
"name": obj.name,
"type": obj.type,
"location": [float(obj.location.x), float(obj.location.y), float(obj.location.z)],
"rotation": [float(obj.rotation_euler.x), float(obj.rotation_euler.y), float(obj.rotation_euler.z)],
"scale": [float(obj.scale.x), float(obj.scale.y), float(obj.scale.z)],
"visible": obj.visible_get(),
}
scene_info["objects"].append(obj_info)
# Collect material information (limit to first 10 materials)
for i, mat in enumerate(bpy.data.materials):
if i >= 10:
break
mat_info = {
"name": mat.name,
"use_nodes": bool(mat.use_nodes),
}
scene_info["materials"].append(mat_info)
# Camera information
camera = bpy.context.scene.camera
if camera:
scene_info["camera"] = {
"name": camera.name,
"location": [float(camera.location.x), float(camera.location.y), float(camera.location.z)],
"rotation": [float(camera.rotation_euler.x), float(camera.rotation_euler.y), float(camera.rotation_euler.z)],
}
# Render settings (simplified)
render = bpy.context.scene.render
scene_info["render_settings"] = {
"engine": render.engine,
"resolution_x": int(render.resolution_x),
"resolution_y": int(render.resolution_y),
}
print(f"Scene info collected: {len(scene_info['objects'])} objects, {len(scene_info['materials'])} materials")
return scene_info
except Exception as e:
print(f"Error in get_scene_info: {str(e)}")
import traceback
traceback.print_exc()
return {"error": str(e)}
def create_object(self, type="CUBE", name=None, location=(0, 0, 0), rotation=(0, 0, 0), scale=(1, 1, 1)):
"""Create a new object in the scene"""
# Deselect all objects
bpy.ops.object.select_all(action='DESELECT')
# Create the object based on type
if type == "CUBE":
bpy.ops.mesh.primitive_cube_add(location=location, rotation=rotation, scale=scale)
elif type == "SPHERE":
bpy.ops.mesh.primitive_uv_sphere_add(location=location, rotation=rotation, scale=scale)
elif type == "CYLINDER":
bpy.ops.mesh.primitive_cylinder_add(location=location, rotation=rotation, scale=scale)
elif type == "PLANE":
bpy.ops.mesh.primitive_plane_add(location=location, rotation=rotation, scale=scale)
elif type == "CONE":
bpy.ops.mesh.primitive_cone_add(location=location, rotation=rotation, scale=scale)
elif type == "TORUS":
bpy.ops.mesh.primitive_torus_add(location=location, rotation=rotation, scale=scale)
elif type == "EMPTY":
bpy.ops.object.empty_add(location=location, rotation=rotation, scale=scale)
elif type == "CAMERA":
bpy.ops.object.camera_add(location=location, rotation=rotation)
elif type == "LIGHT":
bpy.ops.object.light_add(type='POINT', location=location, rotation=rotation, scale=scale)
else:
raise ValueError(f"Unsupported object type: {type}")
# Get the created object
obj = bpy.context.active_object
# Rename if name is provided
if name:
obj.name = name
return {
"name": obj.name,
"type": obj.type,
"location": [obj.location.x, obj.location.y, obj.location.z],
"rotation": [obj.rotation_euler.x, obj.rotation_euler.y, obj.rotation_euler.z],
"scale": [obj.scale.x, obj.scale.y, obj.scale.z],
}
def modify_object(self, name, location=None, rotation=None, scale=None, visible=None):
"""Modify an existing object in the scene"""
# Find the object by name
obj = bpy.data.objects.get(name)
if not obj:
raise ValueError(f"Object not found: {name}")
# Modify properties as requested
if location is not None:
obj.location = location
if rotation is not None:
obj.rotation_euler = rotation
if scale is not None:
obj.scale = scale
if visible is not None:
obj.hide_viewport = not visible
obj.hide_render = not visible
return {
"name": obj.name,
"type": obj.type,
"location": [obj.location.x, obj.location.y, obj.location.z],
"rotation": [obj.rotation_euler.x, obj.rotation_euler.y, obj.rotation_euler.z],
"scale": [obj.scale.x, obj.scale.y, obj.scale.z],
"visible": obj.visible_get(),
}
def delete_object(self, name):
"""Delete an object from the scene"""
obj = bpy.data.objects.get(name)
if not obj:
raise ValueError(f"Object not found: {name}")
# Store the name to return
obj_name = obj.name
# Select and delete the object
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
bpy.ops.object.delete()
return {"deleted": obj_name}
def get_object_info(self, name):
"""Get detailed information about a specific object"""
obj = bpy.data.objects.get(name)
if not obj:
raise ValueError(f"Object not found: {name}")
# Basic object info
obj_info = {
"name": obj.name,
"type": obj.type,
"location": [obj.location.x, obj.location.y, obj.location.z],
"rotation": [obj.rotation_euler.x, obj.rotation_euler.y, obj.rotation_euler.z],
"scale": [obj.scale.x, obj.scale.y, obj.scale.z],
"visible": obj.visible_get(),
"materials": [],
}
# Add material slots
for slot in obj.material_slots:
if slot.material:
obj_info["materials"].append(slot.material.name)
# Add mesh data if applicable
if obj.type == 'MESH' and obj.data:
mesh = obj.data
obj_info["mesh"] = {
"vertices": len(mesh.vertices),
"edges": len(mesh.edges),
"polygons": len(mesh.polygons),
}
return obj_info
def execute_code(self, code):
"""Execute arbitrary Blender Python code"""
# This is powerful but potentially dangerous - use with caution
try:
# Create a local namespace for execution
namespace = {"bpy": bpy}
exec(code, namespace)
return {"executed": True}
except Exception as e:
raise Exception(f"Code execution error: {str(e)}")
def set_material(self, object_name, material_name=None, create_if_missing=True, color=None):
"""Set or create a material for an object"""
obj = bpy.data.objects.get(object_name)
if not obj:
raise ValueError(f"Object not found: {object_name}")
# If material_name is provided, try to find or create the material
if material_name:
mat = bpy.data.materials.get(material_name)
if not mat and create_if_missing:
mat = bpy.data.materials.new(name=material_name)
# Set material color if provided
if color and len(color) >= 3:
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get('Principled BSDF')
if bsdf:
bsdf.inputs[0].default_value = (color[0], color[1], color[2], 1.0 if len(color) < 4 else color[3])
# Assign material to object
if mat:
if obj.data.materials:
obj.data.materials[0] = mat
else:
obj.data.materials.append(mat)
return {"object": object_name, "material": material_name}
# If only color is provided, create a new material with the color
elif color:
# Create a new material with auto-generated name
mat_name = f"{object_name}_material"
mat = bpy.data.materials.new(name=mat_name)
# Set material color
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get('Principled BSDF')
if bsdf:
bsdf.inputs[0].default_value = (color[0], color[1], color[2], 1.0 if len(color) < 4 else color[3])
# Assign material to object
if obj.data.materials:
obj.data.materials[0] = mat
else:
obj.data.materials.append(mat)
return {"object": object_name, "material": mat_name}
else:
return {"error": "Either material_name or color must be provided"}
def render_scene(self, output_path=None, resolution_x=None, resolution_y=None):
"""Render the current scene"""
if resolution_x is not None:
bpy.context.scene.render.resolution_x = resolution_x
if resolution_y is not None:
bpy.context.scene.render.resolution_y = resolution_y
if output_path:
bpy.context.scene.render.filepath = output_path
# Render the scene
bpy.ops.render.render(write_still=bool(output_path))
return {
"rendered": True,
"output_path": output_path if output_path else "[not saved]",
"resolution": [bpy.context.scene.render.resolution_x, bpy.context.scene.render.resolution_y],
}
# Blender UI Panel
class BLENDERMCP_PT_Panel(bpy.types.Panel):
bl_label = "Blender MCP"
bl_idname = "BLENDERMCP_PT_Panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'BlenderMCP'
def draw(self, context):
layout = self.layout
scene = context.scene
layout.prop(scene, "blendermcp_port")
if not scene.blendermcp_server_running:
layout.operator("blendermcp.start_server", text="Start MCP Server")
else:
layout.operator("blendermcp.stop_server", text="Stop MCP Server")
layout.label(text=f"Running on port {scene.blendermcp_port}")
# Operator to start the server
class BLENDERMCP_OT_StartServer(bpy.types.Operator):
bl_idname = "blendermcp.start_server"
bl_label = "Start BlenderMCP Server"
bl_description = "Start the BlenderMCP server to connect with Claude"
def execute(self, context):
scene = context.scene
# Create a new server instance
if not hasattr(bpy.types, "blendermcp_server") or not bpy.types.blendermcp_server:
bpy.types.blendermcp_server = BlenderMCPServer(port=scene.blendermcp_port)
# Start the server
bpy.types.blendermcp_server.start()
scene.blendermcp_server_running = True
return {'FINISHED'}
# Operator to stop the server
class BLENDERMCP_OT_StopServer(bpy.types.Operator):
bl_idname = "blendermcp.stop_server"
bl_label = "Stop BlenderMCP Server"
bl_description = "Stop the BlenderMCP server"
def execute(self, context):
scene = context.scene
# Stop the server if it exists
if hasattr(bpy.types, "blendermcp_server") and bpy.types.blendermcp_server:
bpy.types.blendermcp_server.stop()
del bpy.types.blendermcp_server
scene.blendermcp_server_running = False
return {'FINISHED'}
# Registration functions
def register():
bpy.types.Scene.blendermcp_port = IntProperty(
name="Port",
description="Port for the BlenderMCP server",
default=9876,
min=1024,
max=65535
)
bpy.types.Scene.blendermcp_server_running = bpy.props.BoolProperty(
name="Server Running",
default=False
)
bpy.utils.register_class(BLENDERMCP_PT_Panel)
bpy.utils.register_class(BLENDERMCP_OT_StartServer)
bpy.utils.register_class(BLENDERMCP_OT_StopServer)
print("BlenderMCP addon registered")
def unregister():
# Stop the server if it's running
if hasattr(bpy.types, "blendermcp_server") and bpy.types.blendermcp_server:
bpy.types.blendermcp_server.stop()
del bpy.types.blendermcp_server
bpy.utils.unregister_class(BLENDERMCP_PT_Panel)
bpy.utils.unregister_class(BLENDERMCP_OT_StartServer)
bpy.utils.unregister_class(BLENDERMCP_OT_StopServer)
del bpy.types.Scene.blendermcp_port
del bpy.types.Scene.blendermcp_server_running
print("BlenderMCP addon unregistered")
if __name__ == "__main__":
register()

595
blender_mcp_server.py Normal file
View File

@ -0,0 +1,595 @@
# blender_mcp_server.py
from mcp.server.fastmcp import FastMCP, Context, Image
import socket
import json
import asyncio
import logging
from dataclasses import dataclass
from contextlib import asynccontextmanager
from typing import AsyncIterator, Dict, Any, List
# Configure logging
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger("BlenderMCPServer")
@dataclass
class BlenderConnection:
host: str
port: int
sock: socket.socket = None # Changed from 'socket' to 'sock' to avoid naming conflict
def connect(self) -> bool:
"""Connect to the Blender addon socket server"""
if self.sock:
return True
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((self.host, self.port))
logger.info(f"Connected to Blender at {self.host}:{self.port}")
return True
except Exception as e:
logger.error(f"Failed to connect to Blender: {str(e)}")
self.sock = None
return False
def disconnect(self):
"""Disconnect from the Blender addon"""
if self.sock:
try:
self.sock.close()
except Exception as e:
logger.error(f"Error disconnecting from Blender: {str(e)}")
finally:
self.sock = None
# Replace the single recv call with this chunked approach
def receive_full_response(self, sock, buffer_size=8192):
"""Receive the complete response, potentially in multiple chunks"""
chunks = []
sock.settimeout(10.0)
try:
while True:
chunk = sock.recv(buffer_size)
if not chunk:
break
chunks.append(chunk)
# Check if we've received a complete JSON object
try:
data = b''.join(chunks)
json.loads(data.decode('utf-8'))
# If we get here, it parsed successfully
logger.info(f"Received complete response ({len(data)} bytes)")
return data
except json.JSONDecodeError:
# Incomplete JSON, continue receiving
continue
except socket.timeout:
logger.warning("Socket timeout during chunked receive")
# Return whatever we got
data = b''.join(chunks)
if not data:
raise Exception("Empty response received")
return data
# Then in send_command:
# response_data = self.sock.recv(65536)
def send_command(self, command_type: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
"""Send a command to Blender and return the response"""
if not self.sock and not self.connect():
raise ConnectionError("Not connected to Blender")
command = {
"type": command_type,
"params": params or {}
}
try:
# Log the command being sent
logger.info(f"Sending command: {command_type} with params: {params}")
# Send the command
self.sock.sendall(json.dumps(command).encode('utf-8'))
logger.info(f"Command sent, waiting for response...")
# Set a timeout for receiving
self.sock.settimeout(30.0) # Increased timeout
# Receive the response
# response_data = self.sock.recv(65536) # Increase buffer size for larger responses
response_data = self.receive_full_response(self.sock)
logger.info(f"Received {len(response_data)} bytes of data")
response = json.loads(response_data.decode('utf-8'))
logger.info(f"Response parsed, status: {response.get('status', 'unknown')}")
if response.get("status") == "error":
logger.error(f"Blender error: {response.get('message')}")
raise Exception(response.get("message", "Unknown error from Blender"))
return response.get("result", {})
except socket.timeout:
logger.error("Socket timeout while waiting for response from Blender")
# Try to reconnect
self.disconnect()
if self.connect():
logger.info("Reconnected to Blender after timeout")
raise Exception("Timeout waiting for Blender response")
except json.JSONDecodeError as e:
logger.error(f"Invalid JSON response from Blender: {str(e)}")
# Try to log what was received
if response_data:
logger.error(f"Raw response (first 200 bytes): {response_data[:200]}")
raise Exception(f"Invalid response from Blender: {str(e)}")
except Exception as e:
logger.error(f"Error communicating with Blender: {str(e)}")
# Try to reconnect
self.disconnect()
if self.connect():
logger.info("Reconnected to Blender")
raise Exception(f"Communication error with Blender: {str(e)}")
@asynccontextmanager
async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
"""Manage server startup and shutdown lifecycle"""
blender = BlenderConnection(host="localhost", port=9876)
try:
# Connect to Blender on startup
connected = blender.connect()
if not connected:
logger.warning("Could not connect to Blender on startup. Make sure the Blender addon is running.")
# Return the Blender connection in the context
yield {"blender": blender}
finally:
# Disconnect from Blender on shutdown
blender.disconnect()
logger.info("Disconnected from Blender")
# Create the MCP server with lifespan support
mcp = FastMCP(
"BlenderMCP",
description="Blender integration through the Model Context Protocol",
lifespan=server_lifespan
)
# Resource endpoints
# Global connection for resources (workaround since resources can't access context)
_blender_connection = None
def get_blender_connection():
global _blender_connection
if _blender_connection is None:
_blender_connection = BlenderConnection(host="localhost", port=9876)
_blender_connection.connect()
return _blender_connection
@mcp.resource("blender://ping")
def ping_blender() -> str:
"""Simple ping to test Blender connectivity"""
blender = get_blender_connection()
try:
result = blender.send_command("ping")
return f"Ping successful: {json.dumps(result)}"
except Exception as e:
return f"Ping failed: {str(e)}"
@mcp.resource("blender://simple")
def get_simple_info() -> str:
"""Get simplified information from Blender"""
blender = get_blender_connection()
try:
result = blender.send_command("get_simple_info")
return json.dumps(result, indent=2)
except Exception as e:
return f"Error getting simple info: {str(e)}"
@mcp.resource("blender://scene")
def get_scene_info() -> str:
"""
Get information about the current Blender scene, including all objects,
materials, camera settings, and render configuration.
"""
blender = get_blender_connection()
try:
scene_info = blender.send_command("get_scene_info")
return json.dumps(scene_info, indent=2)
except Exception as e:
return f"Error getting scene info: {str(e)}"
@mcp.resource("blender://object/{object_name}")
def get_object_info(object_name: str) -> str:
"""
Get detailed information about a specific object in the Blender scene.
Parameters:
- object_name: The name of the object to get information about
"""
blender = get_blender_connection()
try:
object_info = blender.send_command("get_object_info", {"name": object_name})
return json.dumps(object_info, indent=2)
except Exception as e:
return f"Error getting object info: {str(e)}"
# Tool endpoints
@mcp.tool()
def create_object(
ctx: Context,
type: str = "CUBE",
name: str = None,
location: List[float] = None,
rotation: List[float] = None,
scale: List[float] = None
) -> str:
"""
Create a new object in the Blender scene.
Parameters:
- type: Object type (CUBE, SPHERE, CYLINDER, PLANE, CONE, TORUS, EMPTY, CAMERA, LIGHT)
- name: Optional name for the object
- location: Optional [x, y, z] location coordinates
- rotation: Optional [x, y, z] rotation in radians
- scale: Optional [x, y, z] scale factors
"""
blender = ctx.request_context.lifespan_context.get("blender")
if not blender:
return "Not connected to Blender"
# Set default values for missing parameters
loc = location or [0, 0, 0]
rot = rotation or [0, 0, 0]
sc = scale or [1, 1, 1]
try:
params = {
"type": type,
"location": loc,
"rotation": rot,
"scale": sc
}
if name:
params["name"] = name
result = blender.send_command("create_object", params)
return f"Created {type} object: {result['name']}"
except Exception as e:
return f"Error creating object: {str(e)}"
@mcp.tool()
def modify_object(
ctx: Context,
name: str,
location: List[float] = None,
rotation: List[float] = None,
scale: List[float] = None,
visible: bool = None
) -> str:
"""
Modify an existing object in the Blender scene.
Parameters:
- name: Name of the object to modify
- location: Optional [x, y, z] location coordinates
- rotation: Optional [x, y, z] rotation in radians
- scale: Optional [x, y, z] scale factors
- visible: Optional boolean to set visibility
"""
blender = ctx.request_context.lifespan_context.get("blender")
if not blender:
return "Not connected to Blender"
try:
params = {"name": name}
if location is not None:
params["location"] = location
if rotation is not None:
params["rotation"] = rotation
if scale is not None:
params["scale"] = scale
if visible is not None:
params["visible"] = visible
result = blender.send_command("modify_object", params)
return f"Modified object: {result['name']}"
except Exception as e:
return f"Error modifying object: {str(e)}"
@mcp.tool()
def delete_object(ctx: Context, name: str) -> str:
"""
Delete an object from the Blender scene.
Parameters:
- name: Name of the object to delete
"""
blender = ctx.request_context.lifespan_context.get("blender")
if not blender:
return "Not connected to Blender"
try:
result = blender.send_command("delete_object", {"name": name})
return f"Deleted object: {result['deleted']}"
except Exception as e:
return f"Error deleting object: {str(e)}"
@mcp.tool()
def set_material(
ctx: Context,
object_name: str,
material_name: str = None,
color: List[float] = None
) -> str:
"""
Set or create a material for an object.
Parameters:
- object_name: Name of the object to assign the material to
- material_name: Optional name of the material to use/create
- color: Optional [r, g, b] or [r, g, b, a] color values (0.0-1.0)
"""
blender = ctx.request_context.lifespan_context.get("blender")
if not blender:
return "Not connected to Blender"
try:
params = {
"object_name": object_name
}
if material_name:
params["material_name"] = material_name
if color:
params["color"] = color
result = blender.send_command("set_material", params)
if "material" in result:
return f"Set material '{result['material']}' on object '{result['object']}'"
else:
return f"Error setting material: {result.get('error', 'Unknown error')}"
except Exception as e:
return f"Error setting material: {str(e)}"
@mcp.tool()
def render_scene(
ctx: Context,
output_path: str = None,
resolution_x: int = None,
resolution_y: int = None
) -> str:
"""
Render the current Blender scene.
Parameters:
- output_path: Optional path to save the rendered image
- resolution_x: Optional horizontal resolution in pixels
- resolution_y: Optional vertical resolution in pixels
"""
blender = ctx.request_context.lifespan_context.get("blender")
if not blender:
return "Not connected to Blender"
try:
params = {}
if output_path:
params["output_path"] = output_path
if resolution_x is not None:
params["resolution_x"] = resolution_x
if resolution_y is not None:
params["resolution_y"] = resolution_y
result = blender.send_command("render_scene", params)
if result.get("rendered"):
if output_path:
return f"Scene rendered and saved to {result['output_path']} at resolution {result['resolution'][0]}x{result['resolution'][1]}"
else:
return f"Scene rendered at resolution {result['resolution'][0]}x{result['resolution'][1]}"
else:
return "Error rendering scene"
except Exception as e:
return f"Error rendering scene: {str(e)}"
@mcp.tool()
def execute_blender_code(ctx: Context, code: str) -> str:
"""
Execute arbitrary Blender Python code.
WARNING: This tool allows executing any Python code in Blender's environment.
Use with caution as it can modify or delete data.
Parameters:
- code: The Python code to execute in Blender's context
"""
blender = ctx.request_context.lifespan_context.get("blender")
if not blender:
return "Not connected to Blender"
try:
result = blender.send_command("execute_code", {"code": code})
if result.get("executed"):
return "Code executed successfully"
else:
return "Error executing code"
except Exception as e:
return f"Error executing code: {str(e)}"
@mcp.tool()
def create_3d_scene(ctx: Context, description: str) -> str:
"""
Create a 3D scene based on a natural language description.
This helper function interprets a description and creates the appropriate objects.
Parameters:
- description: A natural language description of the 3D scene to create
"""
blender = ctx.request_context.lifespan_context.get("blender")
if not blender:
return "Not connected to Blender"
# First, get the current scene to see what's there
try:
scene_info = blender.send_command("get_scene_info")
except Exception as e:
return f"Error accessing Blender scene: {str(e)}"
# Parse the description and create a simple scene
# This is a basic implementation - a more sophisticated version would use
# natural language understanding to interpret the description more accurately
response = "Creating scene based on your description:\n\n"
try:
# Split description into parts
parts = description.lower().split()
# Look for basic scene elements
if any(word in parts for word in ["ground", "floor", "plane"]):
# Create a ground plane
ground = blender.send_command("create_object", {
"type": "PLANE",
"name": "Ground",
"location": [0, 0, 0],
"scale": [5, 5, 1]
})
response += f"✓ Created ground plane '{ground['name']}'\n"
# Set material to gray
blender.send_command("set_material", {
"object_name": ground["name"],
"color": [0.8, 0.8, 0.8]
})
# Look for cubes
if any(word in parts for word in ["cube", "box"]):
cube = blender.send_command("create_object", {
"type": "CUBE",
"name": "Cube",
"location": [0, 0, 1],
"scale": [1, 1, 1]
})
response += f"✓ Created cube '{cube['name']}'\n"
# Set material to blue
blender.send_command("set_material", {
"object_name": cube["name"],
"color": [0.2, 0.4, 0.8]
})
# Look for spheres
if any(word in parts for word in ["sphere", "ball"]):
sphere = blender.send_command("create_object", {
"type": "SPHERE",
"name": "Sphere",
"location": [2, 2, 1],
"scale": [1, 1, 1]
})
response += f"✓ Created sphere '{sphere['name']}'\n"
# Set material to red
blender.send_command("set_material", {
"object_name": sphere["name"],
"color": [0.8, 0.2, 0.2]
})
# Look for cylinders
if any(word in parts for word in ["cylinder", "pipe", "tube"]):
cylinder = blender.send_command("create_object", {
"type": "CYLINDER",
"name": "Cylinder",
"location": [-2, -2, 1],
"scale": [1, 1, 1]
})
response += f"✓ Created cylinder '{cylinder['name']}'\n"
# Set material to green
blender.send_command("set_material", {
"object_name": cylinder["name"],
"color": [0.2, 0.8, 0.2]
})
# Add a camera if not already in the scene
if not any(obj.get("type") == "CAMERA" for obj in scene_info["objects"]):
camera = blender.send_command("create_object", {
"type": "CAMERA",
"name": "Camera",
"location": [7, -7, 5],
"rotation": [0.9, 0, 2.6] # Pointing at the origin
})
response += f"✓ Added camera '{camera['name']}'\n"
# Add a light if not already in the scene
if not any(obj.get("type") == "LIGHT" for obj in scene_info["objects"]):
light = blender.send_command("create_object", {
"type": "LIGHT",
"name": "Light",
"location": [4, 1, 6]
})
response += f"✓ Added light '{light['name']}'\n"
response += "\nScene created! You can continue to modify it with specific commands."
return response
except Exception as e:
return f"Error creating scene: {str(e)}"
# Prompts to help users interact with Blender
@mcp.prompt()
def create_simple_scene() -> str:
"""Create a simple Blender scene with basic objects"""
return """
I'd like to create a simple scene in Blender. Please create:
1. A ground plane
2. A cube above the ground
3. A sphere to the side
4. Make sure there's a camera and light
5. Set different colors for the objects
"""
@mcp.prompt()
def animate_object() -> str:
"""Create keyframe animation for an object"""
return """
I want to animate a cube moving from point A to point B over 30 frames.
Can you help me create this animation?
"""
@mcp.prompt()
def add_material() -> str:
"""Add a material to an object"""
return """
I have a cube in my scene. Can you create a blue metallic material and apply it to the cube?
"""
# Main execution
def main():
"""Run the MCP server"""
mcp.run()
if __name__ == "__main__":
main()

6
main.py Normal file
View File

@ -0,0 +1,6 @@
def main():
print("Hello from blender-mcp!")
if __name__ == "__main__":
main()

9
pyproject.toml Normal file
View File

@ -0,0 +1,9 @@
[project]
name = "blender-mcp"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"mcp[cli]>=1.3.0",
]

43
test_socket_connection.py Normal file
View File

@ -0,0 +1,43 @@
import socket
import json
import time
def test_simple_command():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
print("Connecting to Blender...")
sock.connect(('localhost', 9876))
print("Connected!")
# Simple ping command
command = {
"type": "ping",
"params": {}
}
print(f"Sending command: {json.dumps(command)}")
sock.sendall(json.dumps(command).encode('utf-8'))
print(f"Setting socket timeout: 10 seconds")
sock.settimeout(10)
print("Waiting for response...")
try:
response_data = sock.recv(65536)
print(f"Received {len(response_data)} bytes")
if response_data:
response = json.loads(response_data.decode('utf-8'))
print(f"Response: {response}")
else:
print("Received empty response")
except socket.timeout:
print("Socket timeout while waiting for response")
except Exception as e:
print(f"Error: {type(e).__name__}: {str(e)}")
finally:
sock.close()
if __name__ == "__main__":
test_simple_command()

393
uv.lock generated Normal file
View File

@ -0,0 +1,393 @@
version = 1
revision = 1
requires-python = ">=3.10"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
]
[[package]]
name = "anyio"
version = "4.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
]
[[package]]
name = "blender-mcp"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "mcp", extra = ["cli"] },
]
[package.metadata]
requires-dist = [{ name = "mcp", extras = ["cli"], specifier = ">=1.3.0" }]
[[package]]
name = "certifi"
version = "2025.1.31"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
]
[[package]]
name = "click"
version = "8.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
name = "exceptiongroup"
version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
]
[[package]]
name = "h11"
version = "0.14.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
]
[[package]]
name = "httpcore"
version = "1.0.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
]
[[package]]
name = "httpx-sse"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
]
[[package]]
name = "mcp"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "httpx" },
{ name = "httpx-sse" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "sse-starlette" },
{ name = "starlette" },
{ name = "uvicorn" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6b/b6/81e5f2490290351fc97bf46c24ff935128cb7d34d68e3987b522f26f7ada/mcp-1.3.0.tar.gz", hash = "sha256:f409ae4482ce9d53e7ac03f3f7808bcab735bdfc0fba937453782efb43882d45", size = 150235 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/d2/a9e87b506b2094f5aa9becc1af5178842701b27217fa43877353da2577e3/mcp-1.3.0-py3-none-any.whl", hash = "sha256:2829d67ce339a249f803f22eba5e90385eafcac45c94b00cab6cef7e8f217211", size = 70672 },
]
[package.optional-dependencies]
cli = [
{ name = "python-dotenv" },
{ name = "typer" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
]
[[package]]
name = "pydantic"
version = "2.10.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 },
]
[[package]]
name = "pydantic-core"
version = "2.27.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 },
{ url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 },
{ url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 },
{ url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 },
{ url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 },
{ url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 },
{ url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 },
{ url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 },
{ url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 },
{ url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 },
{ url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 },
{ url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 },
{ url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 },
{ url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 },
{ url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 },
{ url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 },
{ url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 },
{ url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 },
{ url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 },
{ url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 },
{ url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 },
{ url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 },
{ url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 },
{ url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 },
{ url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 },
{ url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 },
{ url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 },
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
{ url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 },
{ url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 },
{ url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 },
{ url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 },
{ url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 },
{ url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 },
{ url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 },
{ url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 },
{ url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 },
{ url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 },
{ url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 },
{ url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 },
{ url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 },
{ url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 },
{ url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 },
{ url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 },
{ url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 },
{ url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 },
{ url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 },
{ url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 },
{ url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 },
{ url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 },
{ url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
{ url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
{ url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
{ url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 },
{ url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 },
{ url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 },
{ url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 },
{ url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 },
{ url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 },
{ url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 },
{ url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 },
{ url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 },
]
[[package]]
name = "pydantic-settings"
version = "2.8.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 },
]
[[package]]
name = "pygments"
version = "2.19.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
]
[[package]]
name = "python-dotenv"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
]
[[package]]
name = "rich"
version = "13.9.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
[[package]]
name = "sse-starlette"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "starlette" },
]
sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 },
]
[[package]]
name = "starlette"
version = "0.46.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/44/b6/fb9a32e3c5d59b1e383c357534c63c2d3caa6f25bf3c59dd89d296ecbaec/starlette-0.46.0.tar.gz", hash = "sha256:b359e4567456b28d473d0193f34c0de0ed49710d75ef183a74a5ce0499324f50", size = 2575568 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/41/94/8af675a62e3c91c2dee47cf92e602cfac86e8767b1a1ac3caf1b327c2ab0/starlette-0.46.0-py3-none-any.whl", hash = "sha256:913f0798bd90ba90a9156383bcf1350a17d6259451d0d8ee27fc0cf2db609038", size = 71991 },
]
[[package]]
name = "typer"
version = "0.15.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "rich" },
{ name = "shellingham" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 },
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
]
[[package]]
name = "uvicorn"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 },
]