AI时代下的编程——matlib与blender快捷编程化、初始MCP

blender编程建模测试

进入blender脚本模式

cpp 复制代码
import bpy
import math

# 清除场景中的所有对象
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()

# 太阳
sun_radius = 5
bpy.ops.mesh.primitive_uv_sphere_add(radius=sun_radius, location=(0, 0, 0))
sun = bpy.context.active_object
sun.name = "Sun"

# 给太阳添加发光材质
sun_material = bpy.data.materials.new(name="SunMaterial")
sun_material.use_nodes = True
nodes = sun_material.node_tree.nodes
links = sun_material.node_tree.links

# 清除默认节点
for node in nodes:
    nodes.remove(node)

# 创建发光节点
emission = nodes.new(type='ShaderNodeEmission')
emission.inputs['Color'].default_value = (1, 1, 0, 1)
emission.inputs['Strength'].default_value = 10

# 创建材质输出节点
output = nodes.new(type='ShaderNodeOutputMaterial')

# 连接节点
links.new(emission.outputs['Emission'], output.inputs['Surface'])
sun.data.materials.append(sun_material)

# 行星数据(名称、轨道半径、半径、公转周期(帧)、自转周期(帧))
planets_data = [
    ("Mercury", 4, 0.3, 20, 10),
    ("Venus", 6, 0.45, 35, 15),
    ("Earth", 8, 0.6, 50, 20),
    ("Mars", 10, 0.54, 70, 22),
    ("Jupiter", 14, 1.8, 120, 30),
    ("Saturn", 18, 1.5, 180, 35),
    ("Uranus", 22, 0.9, 250, 40),
    ("Neptune", 26, 0.9, 320, 45)
]

# 行星颜色
planet_colors = [
    (0.7, 0.7, 0.7, 1),
    (0.8, 0.6, 0.2, 1),
    (0.2, 0.4, 0.8, 1),
    (0.9, 0.4, 0.2, 1),
    (0.9, 0.8, 0.7, 1),
    (0.8, 0.7, 0.5, 1),
    (0.7, 0.8, 0.9, 1),
    (0.4, 0.5, 0.9, 1)
]

# 地球的卫星 - 月球
moon_orbit_radius = 1
moon_radius = 0.15
moon_period = 50

# 存储地球对象
earth = None

for i, (planet_name, orbit_radius, planet_radius, period, rotation_period) in enumerate(planets_data):
    # 创建行星
    bpy.ops.mesh.primitive_uv_sphere_add(radius=planet_radius, location=(orbit_radius, 0, 0))
    planet = bpy.context.active_object
    planet.name = planet_name

    # 给行星添加材质
    planet_material = bpy.data.materials.new(name=f"{planet_name}Material")
    planet_material.use_nodes = True
    nodes = planet_material.node_tree.nodes
    # 清除默认节点
    for node in nodes:
        nodes.remove(node)
    # 创建 Principled BSDF 节点
    bsdf = nodes.new(type='ShaderNodeBsdfPrincipled')
    # 创建材质输出节点
    output = nodes.new(type='ShaderNodeOutputMaterial')
    # 连接节点
    links = planet_material.node_tree.links
    links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])
    bsdf.inputs['Base Color'].default_value = planet_colors[i]
    planet.data.materials.append(planet_material)

    # 创建行星的公转动画
    num_frames = 400
    for frame in range(num_frames):
        angle = 2 * math.pi * frame / period
        x = orbit_radius * math.cos(angle)
        y = orbit_radius * math.sin(angle)
        planet.location = (x, y, 0)
        planet.keyframe_insert(data_path="location", frame=frame)

    # 创建行星的自转动画
    for frame in range(num_frames):
        rotation_angle = 2 * math.pi * frame / rotation_period
        planet.rotation_euler = (0, 0, rotation_angle)
        planet.keyframe_insert(data_path="rotation_euler", frame=frame)

    if planet_name == "Earth":
        earth = planet

# 创建月球
bpy.ops.mesh.primitive_uv_sphere_add(radius=moon_radius, location=(earth.location.x + moon_orbit_radius, earth.location.y, 0))
moon = bpy.context.active_object
moon.name = "Moon"

# 给月球添加发光材质
moon_material = bpy.data.materials.new(name="MoonMaterial")
moon_material.use_nodes = True
nodes = moon_material.node_tree.nodes
links = moon_material.node_tree.links

# 清除默认节点
for node in nodes:
    nodes.remove(node)

# 创建发光节点
emission = nodes.new(type='ShaderNodeEmission')
emission.inputs['Color'].default_value = (0.8, 0.8, 0.8, 1)
emission.inputs['Strength'].default_value = 2

# 创建材质输出节点
output = nodes.new(type='ShaderNodeOutputMaterial')

# 连接节点
links.new(emission.outputs['Emission'], output.inputs['Surface'])
moon.data.materials.append(moon_material)

# 创建月球绕地球的公转动画
for frame in range(num_frames):
    # 先计算地球的位置
    earth_angle = 2 * math.pi * frame / planets_data[2][3]
    earth_x = planets_data[2][1] * math.cos(earth_angle)
    earth_y = planets_data[2][1] * math.sin(earth_angle)

    # 再计算月球相对于地球的位置
    moon_angle = 2 * math.pi * frame / moon_period
    moon_x = earth_x + moon_orbit_radius * math.cos(moon_angle)
    moon_y = earth_y + moon_orbit_radius * math.sin(moon_angle)
    moon.location = (moon_x, moon_y, 0)
    moon.keyframe_insert(data_path="location", frame=frame)

# 设置渲染引擎为 Eevee Next
bpy.context.scene.render.engine = 'BLENDER_EEVEE_NEXT'

新建一个脚本并运行上述程序,可以得到太阳系的自转公转的动画

点击即可播放

添加上相关颜色即可。

matlib脚本搭建simulink测试

matlab 复制代码
% ADRC控制器仿真脚本
clear all;
close all;
clc;

% 系统参数
dt = 0.01;         % 采样时间
t = 0:dt:10;       % 仿真时间
N = length(t);     % 时间步数

% 参考输入(阶跃信号)
r = ones(1, N);    % 目标值为1

% 植物模型参数(二阶系统:y'' = -a*y' - b*y + b*u + d(t))
a = 1;             % 系统阻尼
b = 1;             % 系统增益
b0 = 1;            % 控制器估计增益
d = 0.1*sin(t);    % 外部扰动

% ADRC参数
r_td = 10;         % TD跟踪速度因子
h = dt;            % 采样周期
beta_01 = 100;     % ESO参数
beta_02 = 300;
beta_03 = 1000;
delta = 0.1;       % NLSEF非线性因子
c = 0.5;           % NLSEF阻尼因子
k1 = 10;           % NLSEF比例增益
k2 = 5;            % NLSEF微分增益

% 初始化变量
y = zeros(1, N);   % 系统输出
dy = zeros(1, N);  % 系统输出的一阶导数
u = zeros(1, N);   % 控制输入

% TD变量
v1 = zeros(1, N);  % 跟踪信号
v2 = zeros(1, N);  % 跟踪信号的导数

% ESO变量
z1 = zeros(1, N);  % 状态估计
z2 = zeros(1, N);  % 状态导数估计
z3 = zeros(1, N);  % 扰动估计

% 仿真主循环
for k = 1:N-1
    % 1. TD (跟踪微分器)
    if k == 1
        v1(k) = r(k);
        v2(k) = 0;
    else
        e = v1(k-1) - r(k);
        v1(k) = v1(k-1) + h*v2(k-1);
        v2(k) = v2(k-1) + h*fst(e, v2(k-1), r_td, h);
    end
    
    % 2. ESO (扩展状态观测器)
    if k == 1
        z1(k) = y(k);
        z2(k) = 0;
        z3(k) = 0;
    else
        e = z1(k-1) - y(k);
        z1(k) = z1(k-1) + h*(z2(k-1) - beta_01*e);
        z2(k) = z2(k-1) + h*(z3(k-1) + b0*u(k-1) - beta_02*fal(e, 0.5, delta));
        z3(k) = z3(k-1) - h*beta_03*fal(e, 0.25, delta);
    end
    
    % 3. NLSEF (非线性状态误差反馈)
    e1 = v1(k) - z1(k);    % 位置误差
    e2 = v2(k) - z2(k);    % 速度误差
    u0 = k1*fal(e1, 0.5, delta) + k2*fal(e2, 0.25, delta);  % 非线性反馈
    u(k) = u0 - z3(k)/b0;  % 加入扰动补偿
    
    % 4. 植物模型(二阶系统)
    if k == 1
        y(k) = 0;
        dy(k) = 0;
    else
        ddy = -a*dy(k-1) - b*y(k-1) + b*u(k) + d(k);  % 系统动态
        dy(k) = dy(k-1) + h*ddy;
        y(k) = y(k-1) + h*dy(k-1);
    end
end

% 绘图
figure;
subplot(2,1,1);
plot(t, r, 'r--', 'LineWidth', 1.5, 'DisplayName', 'Reference');
hold on;
plot(t, y, 'b-', 'LineWidth', 1.5, 'DisplayName', 'Output');
grid on;
legend;
title('ADRC Control: Output vs Reference');
xlabel('Time (s)');
ylabel('Amplitude');

subplot(2,1,2);
plot(t, u, 'g-', 'LineWidth', 1.5);
grid on;
title('Control Input');
xlabel('Time (s)');
ylabel('Control Signal');

% 非线性函数定义
function y = fal(e, alpha, delta)
    if abs(e) <= delta
        y = e / (delta^(1-alpha));
    else
        y = (abs(e))^alpha * sign(e);
    end
end

function y = fst(e, v, r, h)
    d = r*h;
    d0 = h*d;
    y = e + h*v;
    a0 = sqrt(d^2 + 8*r*abs(y));
    if abs(y) <= d0
        a = v + y/h;
    else
        a = v + 0.5*(a0 - d)*sign(y);
    end
    if abs(a) <= d
        y = -r*a/d;
    else
        y = -r*sign(a);
    end
end

根据以上两种需求,可以通过设计对应的mcp server,让大模型使用这些工具来实现自主的生成,全程连复制粘贴都是不需要的。

第一个是javascript写的,实现的matlab的mcp:
https://github.com/WilliamCloudQi/matlab-mcp-server

由于我没有学过这个,所以看不懂。

另一个是用python实现的:

bash 复制代码
import os
from pathlib import Path
import base64
import subprocess
import sys
from typing import Optional, Dict, Any
from mcp.server.fastmcp import FastMCP, Image, Context
import io
from contextlib import redirect_stdout

# Get MATLAB path from environment variable with default fallback
MATLAB_PATH = os.getenv('MATLAB_PATH', '/Applications/MATLAB_R2024a.app')

# Initialize FastMCP server with dependencies
mcp = FastMCP(
    "MATLAB",
    dependencies=[
        "mcp[cli]"
    ]
)

def ensure_matlab_engine():
    """Ensure MATLAB engine is installed for the current Python environment."""
    try:
        import matlab.engine
        return True
    except ImportError:
        if not os.path.exists(MATLAB_PATH):
            raise RuntimeError(
                f"MATLAB installation not found at {MATLAB_PATH}. "
                "Please set MATLAB_PATH environment variable to your MATLAB installation directory."
            )
        
        # Try to install MATLAB engine
        engine_setup = Path(MATLAB_PATH) / "extern/engines/python/setup.py"
        if not engine_setup.exists():
            raise RuntimeError(
                f"MATLAB Python engine setup not found at {engine_setup}. "
                "Please verify your MATLAB installation."
            )
        
        print(f"Installing MATLAB engine from {engine_setup}...", file=sys.stderr)
        try:
            subprocess.run(
                [sys.executable, str(engine_setup), "install"],
                check=True,
                capture_output=True,
                text=True
            )
            print("MATLAB engine installed successfully.", file=sys.stderr)
            import matlab.engine
            return True
        except subprocess.CalledProcessError as e:
            raise RuntimeError(
                f"Failed to install MATLAB engine: {e.stderr}\n"
                "Please try installing manually or check your MATLAB installation."
            )

# Try to initialize MATLAB engine
ensure_matlab_engine()
import matlab.engine
eng = matlab.engine.start_matlab()

# Create a directory for MATLAB scripts if it doesn't exist
MATLAB_DIR = Path("matlab_scripts")
MATLAB_DIR.mkdir(exist_ok=True)

@mcp.tool()
def create_matlab_script(script_name: str, code: str) -> str:
    """Create a new MATLAB script file.
    
    Args:
        script_name: Name of the script (without .m extension)
        code: MATLAB code to save
    
    Returns:
        Path to the created script
    """
    if not script_name.isidentifier():
        raise ValueError("Script name must be a valid MATLAB identifier")
    
    script_path = MATLAB_DIR / f"{script_name}.m"
    with open(script_path, 'w') as f:
        f.write(code)
    
    return str(script_path)

@mcp.tool()
def create_matlab_function(function_name: str, code: str) -> str:
    """Create a new MATLAB function file.
    
    Args:
        function_name: Name of the function (without .m extension)
        code: MATLAB function code including function definition
    
    Returns:
        Path to the created function file
    """
    if not function_name.isidentifier():
        raise ValueError("Function name must be a valid MATLAB identifier")
    
    # Verify code starts with function definition
    if not code.strip().startswith('function'):
        raise ValueError("Code must start with function definition")
    
    function_path = MATLAB_DIR / f"{function_name}.m"
    with open(function_path, 'w') as f:
        f.write(code)
    
    return str(function_path)

@mcp.tool()
def execute_matlab_script(script_name: str, args: Optional[Dict[str, Any]] = None) -> dict:
    """Execute a MATLAB script and return results."""
    script_path = MATLAB_DIR / f"{script_name}.m"
    if not script_path.exists():
        raise FileNotFoundError(f"Script {script_name}.m not found")

    # Add script directory to MATLAB path
    eng.addpath(str(MATLAB_DIR))
    
    # Clear previous figures
    eng.close('all', nargout=0)
    
    # Create a temporary file for MATLAB output
    temp_output_file = MATLAB_DIR / f"temp_output_{script_name}.txt"
    
    # Execute the script
    result = {}
    try:
        if args:
            # Convert Python types to MATLAB types
            matlab_args = {k: matlab.double([v]) if isinstance(v, (int, float)) else v 
                         for k, v in args.items()}
            eng.workspace['args'] = matlab_args
        
        # Set up diary to capture output
        eng.eval(f"diary('{temp_output_file}')", nargout=0)
        eng.eval(script_name, nargout=0)
        eng.eval("diary off", nargout=0)
        
        # Read captured output
        if temp_output_file.exists():
            with open(temp_output_file, 'r') as f:
                printed_output = f.read().strip()
            # Clean up temp file
            os.remove(temp_output_file)
        else:
            printed_output = "No output captured"
        
        result['printed_output'] = printed_output
        
        # Rest of your code for figures and workspace variables...
        
        # Capture figures if any were generated
        figures = []
        fig_handles = eng.eval('get(groot, "Children")', nargout=1)
        if fig_handles:
            for i, fig in enumerate(fig_handles):
                # Save figure to temporary file
                temp_file = f"temp_fig_{i}.png"
                eng.eval(f"saveas(figure({i+1}), '{temp_file}')", nargout=0)
                
                # Read the file and convert to base64
                with open(temp_file, 'rb') as f:
                    img_data = f.read()
                figures.append(Image(data=img_data, format='png'))
                
                # Clean up temp file
                os.remove(temp_file)
        
        result['figures'] = figures
        
        # Get workspace variables
        var_names = eng.eval('who', nargout=1)
        for var in var_names:
            if var != 'args':  # Skip the args we passed in
                val = eng.workspace[var]
                # Clean variable name for JSON compatibility
                clean_var_name = var.strip().replace(' ', '_')       
  
                val_str = str(val)
                # Truncate long values to prevent excessive output
                max_length = 1000  # Maximum length for variable values
                if len(val_str) > max_length:
                    val_str = val_str[:max_length] + "... [truncated]"
                
                val = val_str  # Replace the original value with the string representation
                result[clean_var_name] = val
        
    except Exception as e:
        raise RuntimeError(f"MATLAB execution error: {str(e)}")
        
    return result

@mcp.tool()
def call_matlab_function(function_name: str, args: Any) -> dict:
    """Call a MATLAB function with arguments."""
    function_path = MATLAB_DIR / f"{function_name}.m"
    if not function_path.exists():
        raise FileNotFoundError(f"Function {function_name}.m not found")

    # Add function directory to MATLAB path
    eng.addpath(str(MATLAB_DIR))
    
    # Clear previous figures
    eng.close('all', nargout=0)
    
    # Create a temporary file for MATLAB output
    temp_output_file = MATLAB_DIR / f"temp_output_{function_name}.txt"
    
    # Convert Python arguments to MATLAB types
    matlab_args = []
    for arg in args:
        if isinstance(arg, (int, float)):
            matlab_args.append(matlab.double([arg]))
        elif isinstance(arg, list):
            matlab_args.append(matlab.double(arg))
        else:
            matlab_args.append(arg)
    
    result = {}
    try:
        # Set up diary to capture output
        eng.eval(f"diary('{temp_output_file}')", nargout=0)
        
        # Call the function
        output = getattr(eng, function_name)(*matlab_args)
        
        # Turn off diary
        eng.eval("diary off", nargout=0)
        
        # Read captured output
        if temp_output_file.exists():
            with open(temp_output_file, 'r') as f:
                printed_output = f.read().strip()
            # Clean up temp file
            os.remove(temp_output_file)
        else:
            printed_output = "No output captured"
            
        result['output'] = str(output)
        result['printed_output'] = printed_output
        
        # Capture figures - rest of your code remains the same
        figures = []
        fig_handles = eng.eval('get(groot, "Children")', nargout=1)
        if fig_handles:
            for i, fig in enumerate(fig_handles):
                # Save figure to temporary file
                temp_file = f"temp_fig_{i}.png"
                eng.eval(f"saveas(figure({i+1}), '{temp_file}')", nargout=0)
                
                # Read the file and convert to base64
                with open(temp_file, 'rb') as f:
                    img_data = f.read()
                figures.append(Image(data=img_data, format='png'))
                
                # Clean up temp file
                os.remove(temp_file)
        
        result['figures'] = figures
        
    except Exception as e:
        raise RuntimeError(f"MATLAB execution error: {str(e)}")
        
    return result

@mcp.resource("matlab://scripts/{script_name}")
def get_script_content(script_name: str) -> str:
    """Get the content of a MATLAB script.
    
    Args:
        script_name: Name of the script (without .m extension)
    
    Returns:
        Content of the MATLAB script
    """
    script_path = MATLAB_DIR / f"{script_name}.m"
    if not script_path.exists():
        raise FileNotFoundError(f"Script {script_name}.m not found")
    
    with open(script_path) as f:
        return f.read()

if __name__ == "__main__":
    mcp.run(transport='stdio')

blender 的mcp也是通过python实现的
https://github.com/ahujasid/blender-mcp/tree/main

python 复制代码
# 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
import os
from pathlib import Path
import base64
from urllib.parse import urlparse

# 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

    def receive_full_response(self, sock, buffer_size=8192):
        """Receive the complete response, potentially in multiple chunks"""
        chunks = []
        # Use a consistent timeout value that matches the addon's timeout
        sock.settimeout(15.0)  # Match the addon's timeout
        
        try:
            while True:
                try:
                    chunk = sock.recv(buffer_size)
                    if not chunk:
                        # If we get an empty chunk, the connection might be closed
                        if not chunks:  # If we haven't received anything yet, this is an error
                            raise Exception("Connection closed before receiving any data")
                        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:
                    # If we hit a timeout during receiving, break the loop and try to use what we have
                    logger.warning("Socket timeout during chunked receive")
                    break
                except (ConnectionError, BrokenPipeError, ConnectionResetError) as e:
                    logger.error(f"Socket connection error during receive: {str(e)}")
                    raise  # Re-raise to be handled by the caller
        except socket.timeout:
            logger.warning("Socket timeout during chunked receive")
        except Exception as e:
            logger.error(f"Error during receive: {str(e)}")
            raise
            
        # If we get here, we either timed out or broke out of the loop
        # Try to use what we have
        if chunks:
            data = b''.join(chunks)
            logger.info(f"Returning data after receive completion ({len(data)} bytes)")
            try:
                # Try to parse what we have
                json.loads(data.decode('utf-8'))
                return data
            except json.JSONDecodeError:
                # If we can't parse it, it's incomplete
                raise Exception("Incomplete JSON response received")
        else:
            raise Exception("No data received")

    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 - use the same timeout as in receive_full_response
            self.sock.settimeout(15.0)  # Match the addon's timeout
            
            # Receive the response using the improved receive_full_response method
            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")
            # Don't try to reconnect here - let the get_blender_connection handle reconnection
            # Just invalidate the current socket so it will be recreated next time
            self.sock = None
            raise Exception("Timeout waiting for Blender response - try simplifying your request")
        except (ConnectionError, BrokenPipeError, ConnectionResetError) as e:
            logger.error(f"Socket connection error: {str(e)}")
            self.sock = None
            raise Exception(f"Connection to Blender lost: {str(e)}")
        except json.JSONDecodeError as e:
            logger.error(f"Invalid JSON response from Blender: {str(e)}")
            # Try to log what was received
            if 'response_data' in locals() and 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)}")
            # Don't try to reconnect here - let the get_blender_connection handle reconnection
            self.sock = None
            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"""
    # We don't need to create a connection here since we're using the global connection
    # for resources and tools
    
    try:
        # Just log that we're starting up
        logger.info("BlenderMCP server starting up")
        
        # Try to connect to Blender on startup to verify it's available
        try:
            # This will initialize the global connection if needed
            blender = get_blender_connection()
            logger.info("Successfully connected to Blender on startup")
        except Exception as e:
            logger.warning(f"Could not connect to Blender on startup: {str(e)}")
            logger.warning("Make sure the Blender addon is running before using Blender resources or tools")
        
        # Return an empty context - we're using the global connection
        yield {}
    finally:
        # Clean up the global connection on shutdown
        global _blender_connection
        if _blender_connection:
            logger.info("Disconnecting from Blender on shutdown")
            _blender_connection.disconnect()
            _blender_connection = None
        logger.info("BlenderMCP server shut down")

# 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 (since resources can't access context)
_blender_connection = None
_polyhaven_enabled = False  # Add this global variable

def get_blender_connection():
    """Get or create a persistent Blender connection"""
    global _blender_connection, _polyhaven_enabled  # Add _polyhaven_enabled to globals
    
    # If we have an existing connection, check if it's still valid
    if _blender_connection is not None:
        try:
            # First check if PolyHaven is enabled by sending a ping command
            result = _blender_connection.send_command("get_polyhaven_status")
            # Store the PolyHaven status globally
            _polyhaven_enabled = result.get("enabled", False)
            return _blender_connection
        except Exception as e:
            # Connection is dead, close it and create a new one
            logger.warning(f"Existing connection is no longer valid: {str(e)}")
            try:
                _blender_connection.disconnect()
            except:
                pass
            _blender_connection = None
    
    # Create a new connection if needed
    if _blender_connection is None:
        _blender_connection = BlenderConnection(host="localhost", port=9876)
        if not _blender_connection.connect():
            logger.error("Failed to connect to Blender")
            _blender_connection = None
            raise Exception("Could not connect to Blender. Make sure the Blender addon is running.")
        logger.info("Created new persistent connection to Blender")
    
    return _blender_connection


@mcp.tool()
def get_scene_info(ctx: Context) -> str:
    """Get detailed information about the current Blender scene"""
    try:
        blender = get_blender_connection()
        result = blender.send_command("get_scene_info")
        
        # Just return the JSON representation of what Blender sent us
        return json.dumps(result, indent=2)
    except Exception as e:
        logger.error(f"Error getting scene info from Blender: {str(e)}")
        return f"Error getting scene info: {str(e)}"

@mcp.tool()
def get_object_info(ctx: Context, 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
    """
    try:
        blender = get_blender_connection()
        result = blender.send_command("get_object_info", {"name": object_name})
        
        # Just return the JSON representation of what Blender sent us
        return json.dumps(result, indent=2)
    except Exception as e:
        logger.error(f"Error getting object info from Blender: {str(e)}")
        return f"Error getting object info: {str(e)}"



@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,
    # Torus-specific parameters
    align: str = "WORLD",
    major_segments: int = 48,
    minor_segments: int = 12,
    mode: str = "MAJOR_MINOR",
    major_radius: float = 1.0,
    minor_radius: float = 0.25,
    abso_major_rad: float = 1.25,
    abso_minor_rad: float = 0.75,
    generate_uvs: bool = True
) -> 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 (not used for TORUS)
    
    Torus-specific parameters (only used when type == "TORUS"):
    - align: How to align the torus ('WORLD', 'VIEW', or 'CURSOR')
    - major_segments: Number of segments for the main ring
    - minor_segments: Number of segments for the cross-section
    - mode: Dimension mode ('MAJOR_MINOR' or 'EXT_INT')
    - major_radius: Radius from the origin to the center of the cross sections
    - minor_radius: Radius of the torus' cross section
    - abso_major_rad: Total exterior radius of the torus
    - abso_minor_rad: Total interior radius of the torus
    - generate_uvs: Whether to generate a default UV map
    
    Returns:
    A message indicating the created object name.
    """
    try:
        # Get the global connection
        blender = get_blender_connection()
        
        # Set default values for missing parameters
        loc = location or [0, 0, 0]
        rot = rotation or [0, 0, 0]
        sc = scale or [1, 1, 1]
        
        params = {
            "type": type,
            "location": loc,
            "rotation": rot,
        }
        
        if name:
            params["name"] = name

        if type == "TORUS":
            # For torus, the scale is not used.
            params.update({
                "align": align,
                "major_segments": major_segments,
                "minor_segments": minor_segments,
                "mode": mode,
                "major_radius": major_radius,
                "minor_radius": minor_radius,
                "abso_major_rad": abso_major_rad,
                "abso_minor_rad": abso_minor_rad,
                "generate_uvs": generate_uvs
            })
            result = blender.send_command("create_object", params)
            return f"Created {type} object: {result['name']}"
        else:
            # For non-torus objects, include scale
            params["scale"] = sc
            result = blender.send_command("create_object", params)
            return f"Created {type} object: {result['name']}"
    except Exception as e:
        logger.error(f"Error creating object: {str(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
    """
    try:
        # Get the global connection
        blender = get_blender_connection()
        
        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:
        logger.error(f"Error modifying object: {str(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
    """
    try:
        # Get the global connection
        blender = get_blender_connection()
        
        result = blender.send_command("delete_object", {"name": name})
        return f"Deleted object: {name}"
    except Exception as e:
        logger.error(f"Error deleting object: {str(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 apply the material to
    - material_name: Optional name of the material to use or create
    - color: Optional [R, G, B] color values (0.0-1.0)
    """
    try:
        # Get the global connection
        blender = get_blender_connection()
        
        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)
        return f"Applied material to {object_name}: {result.get('material_name', 'unknown')}"
    except Exception as e:
        logger.error(f"Error setting material: {str(e)}")
        return f"Error setting material: {str(e)}"

@mcp.tool()
def execute_blender_code(ctx: Context, code: str) -> str:
    """
    Execute arbitrary Python code in Blender.
    
    Parameters:
    - code: The Python code to execute
    """
    try:
        # Get the global connection
        blender = get_blender_connection()
        
        result = blender.send_command("execute_code", {"code": code})
        return f"Code executed successfully: {result.get('result', '')}"
    except Exception as e:
        logger.error(f"Error executing code: {str(e)}")
        return f"Error executing code: {str(e)}"

@mcp.tool()
def get_polyhaven_categories(ctx: Context, asset_type: str = "hdris") -> str:
    """
    Get a list of categories for a specific asset type on Polyhaven.
    
    Parameters:
    - asset_type: The type of asset to get categories for (hdris, textures, models, all)
    """
    try:
        blender = get_blender_connection()
        if not _polyhaven_enabled:
            return "PolyHaven integration is disabled. Select it in the sidebar in BlenderMCP, then run it again."
        result = blender.send_command("get_polyhaven_categories", {"asset_type": asset_type})
        
        if "error" in result:
            return f"Error: {result['error']}"
        
        # Format the categories in a more readable way
        categories = result["categories"]
        formatted_output = f"Categories for {asset_type}:\n\n"
        
        # Sort categories by count (descending)
        sorted_categories = sorted(categories.items(), key=lambda x: x[1], reverse=True)
        
        for category, count in sorted_categories:
            formatted_output += f"- {category}: {count} assets\n"
        
        return formatted_output
    except Exception as e:
        logger.error(f"Error getting Polyhaven categories: {str(e)}")
        return f"Error getting Polyhaven categories: {str(e)}"

@mcp.tool()
def search_polyhaven_assets(
    ctx: Context,
    asset_type: str = "all",
    categories: str = None
) -> str:
    """
    Search for assets on Polyhaven with optional filtering.
    
    Parameters:
    - asset_type: Type of assets to search for (hdris, textures, models, all)
    - categories: Optional comma-separated list of categories to filter by
    
    Returns a list of matching assets with basic information.
    """
    try:
        blender = get_blender_connection()
        result = blender.send_command("search_polyhaven_assets", {
            "asset_type": asset_type,
            "categories": categories
        })
        
        if "error" in result:
            return f"Error: {result['error']}"
        
        # Format the assets in a more readable way
        assets = result["assets"]
        total_count = result["total_count"]
        returned_count = result["returned_count"]
        
        formatted_output = f"Found {total_count} assets"
        if categories:
            formatted_output += f" in categories: {categories}"
        formatted_output += f"\nShowing {returned_count} assets:\n\n"
        
        # Sort assets by download count (popularity)
        sorted_assets = sorted(assets.items(), key=lambda x: x[1].get("download_count", 0), reverse=True)
        
        for asset_id, asset_data in sorted_assets:
            formatted_output += f"- {asset_data.get('name', asset_id)} (ID: {asset_id})\n"
            formatted_output += f"  Type: {['HDRI', 'Texture', 'Model'][asset_data.get('type', 0)]}\n"
            formatted_output += f"  Categories: {', '.join(asset_data.get('categories', []))}\n"
            formatted_output += f"  Downloads: {asset_data.get('download_count', 'Unknown')}\n\n"
        
        return formatted_output
    except Exception as e:
        logger.error(f"Error searching Polyhaven assets: {str(e)}")
        return f"Error searching Polyhaven assets: {str(e)}"

@mcp.tool()
def download_polyhaven_asset(
    ctx: Context,
    asset_id: str,
    asset_type: str,
    resolution: str = "1k",
    file_format: str = None
) -> str:
    """
    Download and import a Polyhaven asset into Blender.
    
    Parameters:
    - asset_id: The ID of the asset to download
    - asset_type: The type of asset (hdris, textures, models)
    - resolution: The resolution to download (e.g., 1k, 2k, 4k)
    - file_format: Optional file format (e.g., hdr, exr for HDRIs; jpg, png for textures; gltf, fbx for models)
    
    Returns a message indicating success or failure.
    """
    try:
        blender = get_blender_connection()
        result = blender.send_command("download_polyhaven_asset", {
            "asset_id": asset_id,
            "asset_type": asset_type,
            "resolution": resolution,
            "file_format": file_format
        })
        
        if "error" in result:
            return f"Error: {result['error']}"
        
        if result.get("success"):
            message = result.get("message", "Asset downloaded and imported successfully")
            
            # Add additional information based on asset type
            if asset_type == "hdris":
                return f"{message}. The HDRI has been set as the world environment."
            elif asset_type == "textures":
                material_name = result.get("material", "")
                maps = ", ".join(result.get("maps", []))
                return f"{message}. Created material '{material_name}' with maps: {maps}."
            elif asset_type == "models":
                return f"{message}. The model has been imported into the current scene."
            else:
                return message
        else:
            return f"Failed to download asset: {result.get('message', 'Unknown error')}"
    except Exception as e:
        logger.error(f"Error downloading Polyhaven asset: {str(e)}")
        return f"Error downloading Polyhaven asset: {str(e)}"

@mcp.tool()
def set_texture(
    ctx: Context,
    object_name: str,
    texture_id: str
) -> str:
    """
    Apply a previously downloaded Polyhaven texture to an object.
    
    Parameters:
    - object_name: Name of the object to apply the texture to
    - texture_id: ID of the Polyhaven texture to apply (must be downloaded first)
    
    Returns a message indicating success or failure.
    """
    try:
        # Get the global connection
        blender = get_blender_connection()
        
        result = blender.send_command("set_texture", {
            "object_name": object_name,
            "texture_id": texture_id
        })
        
        if "error" in result:
            return f"Error: {result['error']}"
        
        if result.get("success"):
            material_name = result.get("material", "")
            maps = ", ".join(result.get("maps", []))
            
            # Add detailed material info
            material_info = result.get("material_info", {})
            node_count = material_info.get("node_count", 0)
            has_nodes = material_info.get("has_nodes", False)
            texture_nodes = material_info.get("texture_nodes", [])
            
            output = f"Successfully applied texture '{texture_id}' to {object_name}.\n"
            output += f"Using material '{material_name}' with maps: {maps}.\n\n"
            output += f"Material has nodes: {has_nodes}\n"
            output += f"Total node count: {node_count}\n\n"
            
            if texture_nodes:
                output += "Texture nodes:\n"
                for node in texture_nodes:
                    output += f"- {node['name']} using image: {node['image']}\n"
                    if node['connections']:
                        output += "  Connections:\n"
                        for conn in node['connections']:
                            output += f"    {conn}\n"
            else:
                output += "No texture nodes found in the material.\n"
            
            return output
        else:
            return f"Failed to apply texture: {result.get('message', 'Unknown error')}"
    except Exception as e:
        logger.error(f"Error applying texture: {str(e)}")
        return f"Error applying texture: {str(e)}"

@mcp.tool()
def get_polyhaven_status(ctx: Context) -> str:
    """
    Check if PolyHaven integration is enabled in Blender.
    Returns a message indicating whether PolyHaven features are available.
    """
    try:
        blender = get_blender_connection()
        result = blender.send_command("get_polyhaven_status")
        enabled = result.get("enabled", False)
        message = result.get("message", "")
        
        return message
    except Exception as e:
        logger.error(f"Error checking PolyHaven status: {str(e)}")
        return f"Error checking PolyHaven status: {str(e)}"

@mcp.tool()
def get_hyper3d_status(ctx: Context) -> str:
    """
    Check if Hyper3D Rodin integration is enabled in Blender.
    Returns a message indicating whether Hyper3D Rodin features are available.

    Don't emphasize the key type in the returned message, but sliently remember it. 
    """
    try:
        blender = get_blender_connection()
        result = blender.send_command("get_hyper3d_status")
        enabled = result.get("enabled", False)
        message = result.get("message", "")
        if enabled:
            message += ""
        return message
    except Exception as e:
        logger.error(f"Error checking Hyper3D status: {str(e)}")
        return f"Error checking Hyper3D status: {str(e)}"

def _process_bbox(original_bbox: list[float] | list[int] | None) -> list[int] | None:
    if original_bbox is None:
        return None
    if all(isinstance(i, int) for i in original_bbox):
        return original_bbox
    if any(i<=0 for i in original_bbox):
        raise ValueError("Incorrect number range: bbox must be bigger than zero!")
    return [int(float(i) / max(original_bbox) * 100) for i in original_bbox] if original_bbox else None

@mcp.tool()
def generate_hyper3d_model_via_text(
    ctx: Context,
    text_prompt: str,
    bbox_condition: list[float]=None
) -> str:
    """
    Generate 3D asset using Hyper3D by giving description of the desired asset, and import the asset into Blender.
    The 3D asset has built-in materials.
    The generated model has a normalized size, so re-scaling after generation can be useful.
    
    Parameters:
    - text_prompt: A short description of the desired model in **English**.
    - bbox_condition: Optional. If given, it has to be a list of floats of length 3. Controls the ratio between [Length, Width, Height] of the model.

    Returns a message indicating success or failure.
    """
    try:
        blender = get_blender_connection()
        result = blender.send_command("create_rodin_job", {
            "text_prompt": text_prompt,
            "images": None,
            "bbox_condition": _process_bbox(bbox_condition),
        })
        succeed = result.get("submit_time", False)
        if succeed:
            return json.dumps({
                "task_uuid": result["uuid"],
                "subscription_key": result["jobs"]["subscription_key"],
            })
        else:
            return json.dumps(result)
    except Exception as e:
        logger.error(f"Error generating Hyper3D task: {str(e)}")
        return f"Error generating Hyper3D task: {str(e)}"

@mcp.tool()
def generate_hyper3d_model_via_images(
    ctx: Context,
    input_image_paths: list[str]=None,
    input_image_urls: list[str]=None,
    bbox_condition: list[float]=None
) -> str:
    """
    Generate 3D asset using Hyper3D by giving images of the wanted asset, and import the generated asset into Blender.
    The 3D asset has built-in materials.
    The generated model has a normalized size, so re-scaling after generation can be useful.
    
    Parameters:
    - input_image_paths: The **absolute** paths of input images. Even if only one image is provided, wrap it into a list. Required if Hyper3D Rodin in MAIN_SITE mode.
    - input_image_urls: The URLs of input images. Even if only one image is provided, wrap it into a list. Required if Hyper3D Rodin in FAL_AI mode.
    - bbox_condition: Optional. If given, it has to be a list of ints of length 3. Controls the ratio between [Length, Width, Height] of the model.

    Only one of {input_image_paths, input_image_urls} should be given at a time, depending on the Hyper3D Rodin's current mode.
    Returns a message indicating success or failure.
    """
    if input_image_paths is not None and input_image_urls is not None:
        return f"Error: Conflict parameters given!"
    if input_image_paths is None and input_image_urls is None:
        return f"Error: No image given!"
    if input_image_paths is not None:
        if not all(os.path.exists(i) for i in input_image_paths):
            return "Error: not all image paths are valid!"
        images = []
        for path in input_image_paths:
            with open(path, "rb") as f:
                images.append(
                    (Path(path).suffix, base64.b64encode(f.read()).decode("ascii"))
                )
    elif input_image_urls is not None:
        if not all(urlparse(i) for i in input_image_paths):
            return "Error: not all image URLs are valid!"
        images = input_image_urls.copy()
    try:
        blender = get_blender_connection()
        result = blender.send_command("create_rodin_job", {
            "text_prompt": None,
            "images": images,
            "bbox_condition": _process_bbox(bbox_condition),
        })
        succeed = result.get("submit_time", False)
        if succeed:
            return json.dumps({
                "task_uuid": result["uuid"],
                "subscription_key": result["jobs"]["subscription_key"],
            })
        else:
            return json.dumps(result)
    except Exception as e:
        logger.error(f"Error generating Hyper3D task: {str(e)}")
        return f"Error generating Hyper3D task: {str(e)}"

@mcp.tool()
def poll_rodin_job_status(
    ctx: Context,
    subscription_key: str=None,
    request_id: str=None,
):
    """
    Check if the Hyper3D Rodin generation task is completed.

    For Hyper3D Rodin mode MAIN_SITE:
        Parameters:
        - subscription_key: The subscription_key given in the generate model step.

        Returns a list of status. The task is done if all status are "Done".
        If "Failed" showed up, the generating process failed.
        This is a polling API, so only proceed if the status are finally determined ("Done" or "Canceled").

    For Hyper3D Rodin mode FAL_AI:
        Parameters:
        - request_id: The request_id given in the generate model step.

        Returns the generation task status. The task is done if status is "COMPLETED".
        The task is in progress if status is "IN_PROGRESS".
        If status other than "COMPLETED", "IN_PROGRESS", "IN_QUEUE" showed up, the generating process might be failed.
        This is a polling API, so only proceed if the status are finally determined ("COMPLETED" or some failed state).
    """
    try:
        blender = get_blender_connection()
        kwargs = {}
        if subscription_key:
            kwargs = {
                "subscription_key": subscription_key,
            }
        elif request_id:
            kwargs = {
                "request_id": request_id,
            }
        result = blender.send_command("poll_rodin_job_status", kwargs)
        return result
    except Exception as e:
        logger.error(f"Error generating Hyper3D task: {str(e)}")
        return f"Error generating Hyper3D task: {str(e)}"

@mcp.tool()
def import_generated_asset(
    ctx: Context,
    name: str,
    task_uuid: str=None,
    request_id: str=None,
):
    """
    Import the asset generated by Hyper3D Rodin after the generation task is completed.

    Parameters:
    - name: The name of the object in scene
    - task_uuid: For Hyper3D Rodin mode MAIN_SITE: The task_uuid given in the generate model step.
    - request_id: For Hyper3D Rodin mode FAL_AI: The request_id given in the generate model step.

    Only give one of {task_uuid, request_id} based on the Hyper3D Rodin Mode!
    Return if the asset has been imported successfully.
    """
    try:
        blender = get_blender_connection()
        kwargs = {
            "name": name
        }
        if task_uuid:
            kwargs["task_uuid"] = task_uuid
        elif request_id:
            kwargs["request_id"] = request_id
        result = blender.send_command("import_generated_asset", kwargs)
        return result
    except Exception as e:
        logger.error(f"Error generating Hyper3D task: {str(e)}")
        return f"Error generating Hyper3D task: {str(e)}"

@mcp.prompt()
def asset_creation_strategy() -> str:
    """Defines the preferred strategy for creating assets in Blender"""
    return """When creating 3D content in Blender, always start by checking if integrations are available:

    0. Before anything, always check the scene from get_scene_info()
    1. First use the following tools to verify if the following integrations are enabled:
        1. PolyHaven
            Use get_polyhaven_status() to verify its status
            If PolyHaven is enabled:
            - For objects/models: Use download_polyhaven_asset() with asset_type="models"
            - For materials/textures: Use download_polyhaven_asset() with asset_type="textures"
            - For environment lighting: Use download_polyhaven_asset() with asset_type="hdris"
        2. Hyper3D(Rodin)
            Hyper3D Rodin is good at generating 3D models for single item.
            So don't try to:
            1. Generate the whole scene with one shot
            2. Generate ground using Rodin
            3. Generate parts of the items separately and put them together afterwards

            Use get_hyper3d_status() to verify its status
            If Hyper3D is enabled:
            - For objects/models, do the following steps:
                1. Create the model generation task
                    - Use generate_hyper3d_model_via_images() if image(s) is/are given
                    - Use generate_hyper3d_model_via_text() if generating 3D asset using text prompt
                    If key type is free_trial and insufficient balance error returned, tell the user that the free trial key can only generated limited models everyday, they can choose to:
                    - Wait for another day and try again
                    - Go to hyper3d.ai to find out how to get their own API key
                    - Go to fal.ai to get their own private API key
                2. Poll the status
                    - Use poll_rodin_job_status() to check if the generation task has completed or failed
                3. Import the asset
                    - Use import_generated_asset() to import the generated GLB model the asset
                4. After importing the asset, ALWAYS check the world_bounding_box of the imported mesh, and adjust the mesh's location and size
                    Adjust the imported mesh's location, scale, rotation, so that the mesh is on the right spot.

                You can reuse assets previous generated by running python code to duplicate the object, without creating another generation task.

    2. If all integrations are disabled or when falling back to basic tools:
       - create_object() for basic primitives (CUBE, SPHERE, CYLINDER, etc.)
       - set_material() for basic colors and materials
    
    3. When including an object into scene, ALWAYS make sure that the name of the object is meanful.

    4. Always check the world_bounding_box for each item so that:
        - Ensure that all objects that should not be clipping are not clipping.
        - Items have right spatial relationship.
    
    5. After giving the tool location/scale/rotation information (via create_object() and modify_object()),
       double check the related object's location, scale, rotation, and world_bounding_box using get_object_info(),
       so that the object is in the desired location.

    Only fall back to basic creation tools when:
    - PolyHaven and Hyper3D are disabled
    - A simple primitive is explicitly requested
    - No suitable PolyHaven asset exists
    - Hyper3D Rodin failed to generate the desired asset
    - The task specifically requires a basic material/color
    """

# Main execution

def main():
    """Run the MCP server"""
    mcp.run()

if __name__ == "__main__":
    main()

我自己也通过b站的教程从零写了一个客户端和服务端的mcp,用于让打模型查天气,调用相应的工具实现的。后面整理一下写一篇教程吧。

相关推荐
Panesle22 分钟前
transformer架构与其它架构对比
人工智能·深度学习·transformer
SoFlu软件机器人28 分钟前
Go/Rust 疯狂蚕食 Java 市场?老牌语言的 AI 化自救之路
java·golang·rust
半盏茶香30 分钟前
启幕数据结构算法雅航新章,穿梭C++梦幻领域的探索之旅——堆的应用之堆排、Top-K问题
java·开发语言·数据结构·c++·python·算法·链表
hweiyu0039 分钟前
idea如何让打开的文件名tab多行显示
java·ide·intellij-idea·idea·intellij idea
我有医保我先冲1 小时前
AI大模型与人工智能的深度融合:重构医药行业数字化转型的底层逻辑
人工智能·重构
小吴先生6661 小时前
Groovy 规则执行器,加载到缓存
java·开发语言·缓存·groovy
星星不打輰1 小时前
Spring基于注解进行开发
java·spring
陈大爷(有低保)1 小时前
Spring中都用到了哪些设计模式
java·后端·spring
骑牛小道士1 小时前
JAVA- 锁机制介绍 进程锁
java·开发语言