connecting but request timeout in the socket
This commit is contained in:
commit
4372edf2a7
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
||||
3.13.2
|
||||
499
MCP.md
Normal file
499
MCP.md
Normal 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
517
README.md
Normal 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
511
blender_mcp_addon.py
Normal 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
595
blender_mcp_server.py
Normal 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
6
main.py
Normal file
@ -0,0 +1,6 @@
|
||||
def main():
|
||||
print("Hello from blender-mcp!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
9
pyproject.toml
Normal file
9
pyproject.toml
Normal 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
43
test_socket_connection.py
Normal 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
393
uv.lock
generated
Normal 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 },
|
||||
]
|
||||
Loading…
Reference in New Issue
Block a user