Claude DesktopのMCPでPythonコードを実行するまで(MCPサーバー入門）

### Claude DesktopのMCPでPythonコードを実行するまで（MCPサーバー入門）
MacbookのClaudeのMCPでPythonコードを実行したかったので試してみた。

(本記事は、https://qiita.com/quittardis/items/a3ba1cc395bcb1c29570 の内容を前提知識としています）
上の記事に従って設定後、

#### MCPライブラリをインストールしてプロジェクトを作成
```
# uvを使って直接MCPライブラリをインストールしてプロジェクトを作成
uv init my-python-server
cd my-python-server
uv add "mcp[cli]"
```

基本的なサーバーファイルを作成：
```
# サーバーファイルを作成
touch server.py
```
server.pyに以下の基本的なMCPサーバーコードを追加：

```
from mcp.server.fastmcp import FastMCP
import math
import json
from datetime import datetime

# MCPサーバーを初期化
mcp = FastMCP("MyPythonServer")

# 基本的なツール
@mcp.tool()
def hello(name: str) -> str:
    """Say hello to someone"""
    return f"Hello, {name}! Welcome to my Python MCP server."

@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers"""
    return a + b

@mcp.tool()
def multiply(a: float, b: float) -> float:
    """Multiply two numbers"""
    return a * b

@mcp.tool()
def calculate_circle_area(radius: float) -> float:
    """Calculate the area of a circle given its radius"""
    return math.pi * radius ** 2

@mcp.tool()
def get_current_time() -> str:
    """Get the current date and time"""
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

@mcp.tool()
def reverse_string(text: str) -> str:
    """Reverse a given string"""
    return text[::-1]

@mcp.tool()
def word_count(text: str) -> dict:
    """Count the number of words, characters, and lines in a text"""
    words = len(text.split())
    characters = len(text)
    lines = len(text.split('\n'))
    
    return {
        "words": words,
        "characters": characters,
        "lines": lines
    }

# リソースの追加（オプション）
@mcp.resource("info://server")
def get_server_info() -> str:
    """Get information about this MCP server"""
    return json.dumps({
        "name": "MyPythonServer",
        "version": "1.0.0",
        "description": "A basic Python MCP server with common utilities",
        "tools": [
            "hello", "add", "multiply", "calculate_circle_area", 
            "get_current_time", "reverse_string", "word_count"
        ]
    }, indent=2)

if __name__ == "__main__":
    mcp.run(transport="stdio")
```
（このコードはテストに使うだけですが、MCPサーバーをどうやって
構築するかのいい勉強になります。）

#### Claude Desktopの設定ファイルを編集：

<#ユーザー名>　のところは実際にユーザー名に入れ替えてください。

```
{
  "mcpServers": {
    "my-python-server": {
      "command": "uv",
      "args": [
        "--directory",
        "/Users/#ユーザー名/my-python-server",
        "run",
        "server.py"
      ]
    }
  }
}
```
Claude Desktopを終了し、再起動して、
「今の時刻を教えて」とか聞くとこの新しい serverが動作していることがわかる。
「hello」と呼びかけると

```
Hello! MCPサーバーの機能を使ってお挨拶させていただきました。何かお手伝いできることはありますか？
先ほど作成したmy-python-server MCPの機能も利用できます。例えば：

計算（足し算、掛け算）
円の面積計算
文字列の操作
テキストの統計

何か試してみたいことがあれば、お気軽にお声がけください！
```
次に、
「my-python-serverをpythonコードを実行するMCPにしたい」
と、インプットすると、python実行用のコードが生成される。
```
from mcp.server.fastmcp import FastMCP
import subprocess
import sys
import tempfile
import os
import json
from datetime import datetime
import traceback

# MCPサーバーを初期化
mcp = FastMCP("PythonExecutorServer")

@mcp.tool()
def execute_python_code(code: str, timeout: int = 30) -> dict:
    """
    Execute Python code safely and return the output.
    
    Args:
        code: Python code to execute
        timeout: Maximum execution time in seconds (default: 30)
    
    Returns:
        Dictionary with stdout, stderr, return_code, and execution_time
    """
    try:
        # 一時ファイルを作成してコードを保存
        with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
            f.write(code)
            temp_file = f.name
        
        start_time = datetime.now()
        
        # Pythonコードを実行
        result = subprocess.run(
            [sys.executable, temp_file],
            capture_output=True,
            text=True,
            timeout=timeout
        )
        
        end_time = datetime.now()
        execution_time = (end_time - start_time).total_seconds()
        
        # 一時ファイルを削除
        os.unlink(temp_file)
        
        return {
            "stdout": result.stdout,
            "stderr": result.stderr,
            "return_code": result.returncode,
            "execution_time": execution_time,
            "success": result.returncode == 0
        }
        
    except subprocess.TimeoutExpired:
        # タイムアウトの場合
        if 'temp_file' in locals():
            os.unlink(temp_file)
        return {
            "stdout": "",
            "stderr": f"Execution timed out after {timeout} seconds",
            "return_code": -1,
            "execution_time": timeout,
            "success": False
        }
    except Exception as e:
        # その他のエラー
        if 'temp_file' in locals():
            os.unlink(temp_file)
        return {
            "stdout": "",
            "stderr": f"Error executing code: {str(e)}",
            "return_code": -1,
            "execution_time": 0,
            "success": False
        }

@mcp.tool()
def execute_python_expression(expression: str) -> dict:
    """
    Evaluate a Python expression and return the result.
    
    Args:
        expression: Python expression to evaluate
    
    Returns:
        Dictionary with result, error, and success status
    """
    try:
        # 安全な環境で式を評価
        result = eval(expression, {"__builtins__": {}}, {
            "abs": abs, "round": round, "min": min, "max": max,
            "sum": sum, "len": len, "str": str, "int": int, "float": float,
            "list": list, "dict": dict, "tuple": tuple, "set": set,
            "range": range, "enumerate": enumerate, "zip": zip,
            "sorted": sorted, "reversed": reversed,
            "math": __import__("math"),
            "datetime": __import__("datetime"),
        })
        
        return {
            "result": str(result),
            "type": type(result).__name__,
            "success": True,
            "error": None
        }
        
    except Exception as e:
        return {
            "result": None,
            "type": None,
            "success": False,
            "error": str(e)
        }

@mcp.tool()
def install_package(package_name: str) -> dict:
    """
    Install a Python package using pip.
    
    Args:
        package_name: Name of the package to install
    
    Returns:
        Dictionary with installation status and output
    """
    try:
        result = subprocess.run(
            [sys.executable, "-m", "pip", "install", package_name],
            capture_output=True,
            text=True,
            timeout=300  # 5分のタイムアウト
        )
        
        return {
            "success": result.returncode == 0,
            "stdout": result.stdout,
            "stderr": result.stderr,
            "return_code": result.returncode,
            "package": package_name
        }
        
    except subprocess.TimeoutExpired:
        return {
            "success": False,
            "stdout": "",
            "stderr": f"Package installation timed out after 300 seconds",
            "return_code": -1,
            "package": package_name
        }
    except Exception as e:
        return {
            "success": False,
            "stdout": "",
            "stderr": f"Error installing package: {str(e)}",
            "return_code": -1,
            "package": package_name
        }

@mcp.tool()
def list_installed_packages() -> dict:
    """
    List all installed Python packages.
    
    Returns:
        Dictionary with list of installed packages
    """
    try:
        result = subprocess.run(
            [sys.executable, "-m", "pip", "list", "--format=json"],
            capture_output=True,
            text=True,
            timeout=30
        )
        
        if result.returncode == 0:
            packages = json.loads(result.stdout)
            return {
                "success": True,
                "packages": packages,
                "count": len(packages)
            }
        else:
            return {
                "success": False,
                "error": result.stderr,
                "packages": [],
                "count": 0
            }
            
    except Exception as e:
        return {
            "success": False,
            "error": str(e),
            "packages": [],
            "count": 0
        }

@mcp.tool()
def check_syntax(code: str) -> dict:
    """
    Check Python code syntax without executing it.
    
    Args:
        code: Python code to check
    
    Returns:
        Dictionary with syntax check results
    """
    try:
        compile(code, '<string>', 'exec')
        return {
            "valid": True,
            "error": None,
            "message": "Syntax is valid"
        }
    except SyntaxError as e:
        return {
            "valid": False,
            "error": "SyntaxError",
            "message": str(e),
            "line": e.lineno,
            "offset": e.offset
        }
    except Exception as e:
        return {
            "valid": False,
            "error": type(e).__name__,
            "message": str(e),
            "line": None,
            "offset": None
        }

# サーバー情報リソース
@mcp.resource("info://server")
def get_server_info() -> str:
    """Get information about this Python executor MCP server"""
    return json.dumps({
        "name": "PythonExecutorServer",
        "version": "1.0.0",
        "description": "A Python MCP server that can execute Python code safely",
        "tools": [
            "execute_python_code",
            "execute_python_expression", 
            "install_package",
            "list_installed_packages",
            "check_syntax"
        ],
        "python_version": sys.version,
        "python_executable": sys.executable
    }, indent=2)

if __name__ == "__main__":
    mcp.run(transport="stdio")
```
server.pyにこれをコピペします。


既存のserver.pyファイルの中身を上記のコードで置き換えてください。このアップデートされたMCPサーバーには以下の機能が含まれています：

主な機能
#### 1. execute_python_code(code, timeout=30)

完全なPythonコードを実行
タイムアウト設定可能
標準出力・エラー出力・実行時間を返す

#### 2. execute_python_expression(expression)

数式や簡単な式を安全に評価
制限された環境で実行（セキュリティのため）

#### 3. install_package(package_name)

pipを使ってパッケージをインストール

#### 4. list_installed_packages()

インストール済みパッケージの一覧表示

#### 5. check_syntax(code)

コードの構文チェック（実行なし）


Claude Desktopを再起動すると、以下のようなことができる：

#### コード実行: 

"次のPythonコードを実行して: print('Hello World')"


（これで、my-python-serverがPythonコード実行専用のMCPサーバーになります！）

### numpyや、matplotlibをインストールする場合は、
my_python_serverフォルダに移動し、Terminalから、
```
uv add numpy

uv add matplotlib

uv add opencv-python-headless (ビルドするので時間がかかります）
```
とします。

「Pythonでmatplotlibを使ったサンプル画像を生成して」
とか指示すると、以下のような画像が生成される。

![circle_art.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430586/6feed2c3-cebb-4e82-9ad2-323d3a2f641b.png)

![flower_pattern.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430586/58140c1e-41b6-4343-b7ea-ad08eb646eef.png)

#### Windows版

Macbookで動作した上のコードであるが、Windowsでは、Pythonコードの実行のところでTime Outになってしまう。 ChatGPTやClaudeに相談して色々やってみたが、なかなか正解にたどりつけなかった。最後にPythonコードの実行時のWindowsの問題じゃないかと、あたりをつけ、ChatGPTに簡単なコードを生成し、それを上のコードに移植してやっと動いたのが下のコード。
この二つのコードをClaudeに比較してもらったときのメッセージによると、

「WindowsとmacOSでの動作の違いは、主にプロセス作成のコストの差にあります。ファイル1ではsubprocess.run()で新しいPythonプロセスを起動していますが、Windowsではプロセス作成のオーバーヘッドが非常に大きく、これがタイムアウトの原因となっています。一方、ファイル2ではexec()を使って同一プロセス内でコードを実行するため、プロセス作成のコストを回避できています。macOSやLinuxではfork()システムコールによる軽量なプロセス作成が可能ですが、Windowsではプロセス作成により多くの時間とリソースが必要になります。また、一時ファイルの作成・削除やPythonインタープリターの起動時間も、Windowsの方が一般的に時間がかかります。ただし、ファイル2のアプローチはセキュリティ面でリスクがあるため、本格的な用途では プラットフォーム別の最適化やハイブリッドアプローチを検討することをお勧めします。」

という御託宣でした。

### windows用の改良版👇
def execute_python_codeだけを修正しています

```
from mcp.server.fastmcp import FastMCP
from starlette.responses import JSONResponse
import subprocess
import sys
import tempfile
import os
import json
from datetime import datetime
import traceback

# MCPサーバーを初期化
mcp = FastMCP("PythonExecutorServer")


@mcp.tool()
def execute_python_code(code: str, timeout: int = 30) -> dict:
    """
    Execute Python code safely and return the output.
    
    Args:
        code: Python code to execute
        timeout: Maximum execution time in seconds (default: 30)
    
    Returns:
        Dictionary with stdout, stderr, return_code, and execution_time
    """
    try:
        # 危険なコードの実行を避けるため、実行環境を制限
        exec_globals = {}
        exec_locals = {}

        # 安全な範囲でPythonコードを実行
        exec(code, exec_globals, exec_locals)

        return JSONResponse({
            "status": "success",
            "result": exec_locals
        })

    except Exception as e:
        return JSONResponse({
            "status": "error",
            "message": str(e)
        }, status_code=400)


@mcp.tool()
def execute_python_expression(expression: str) -> dict:
    """
    Evaluate a Python expression and return the result.
    
    Args:
        expression: Python expression to evaluate
    
    Returns:
        Dictionary with result, error, and success status
    """
    try:
        # 安全な環境で式を評価
        result = eval(expression, {"__builtins__": {}}, {
            "abs": abs, "round": round, "min": min, "max": max,
            "sum": sum, "len": len, "str": str, "int": int, "float": float,
            "list": list, "dict": dict, "tuple": tuple, "set": set,
            "range": range, "enumerate": enumerate, "zip": zip,
            "sorted": sorted, "reversed": reversed,
            "math": __import__("math"),
            "datetime": __import__("datetime"),
        })
        
        return {
            "result": str(result),
            "type": type(result).__name__,
            "success": True,
            "error": None
        }
        
    except Exception as e:
        return {
            "result": None,
            "type": None,
            "success": False,
            "error": str(e)
        }

@mcp.tool()
def install_with_uv(package_name: str):
    """
    uv pip を使用してパッケージをインストールする関数。
    uv コマンドがシステムにインストールされている必要があります。
    """
    try:
        print(f"Installing '{package_name}' using uv...")
        subprocess.check_call(["uv", "pip", "install", package_name])
        print(f"'{package_name}' installed successfully with uv.")
    except subprocess.CalledProcessError:
        print(f"Failed to install '{package_name}' using uv.")
    except FileNotFoundError:
        print("❌ 'uv' command not found. Please install uv first.")

@mcp.tool()
def list_installed_packages() -> dict:
    """
    List all installed Python packages.
    
    Returns:
        Dictionary with list of installed packages
    """
    try:
        result = subprocess.run(
            [sys.executable, "-m", "pip", "list", "--format=json"],
            capture_output=True,
            text=True,
            timeout=30
        )
        
        if result.returncode == 0:
            packages = json.loads(result.stdout)
            return {
                "success": True,
                "packages": packages,
                "count": len(packages)
            }
        else:
            return {
                "success": False,
                "error": result.stderr,
                "packages": [],
                "count": 0
            }
            
    except Exception as e:
        return {
            "success": False,
            "error": str(e),
            "packages": [],
            "count": 0
        }

@mcp.tool()
def check_syntax(code: str) -> dict:
    """
    Check Python code syntax without executing it.
    
    Args:
        code: Python code to check
    
    Returns:
        Dictionary with syntax check results
    """
    try:
        compile(code, '<string>', 'exec')
        return {
            "valid": True,
            "error": None,
            "message": "Syntax is valid"
        }
    except SyntaxError as e:
        return {
            "valid": False,
            "error": "SyntaxError",
            "message": str(e),
            "line": e.lineno,
            "offset": e.offset
        }
    except Exception as e:
        return {
            "valid": False,
            "error": type(e).__name__,
            "message": str(e),
            "line": None,
            "offset": None
        }

# サーバー情報リソース
@mcp.resource("info://server")
def get_server_info() -> str:
    """Get information about this Python executor MCP server"""
    return json.dumps({
        "name": "PythonExecutorServer",
        "version": "1.0.0",
        "description": "A Python MCP server that can execute Python code safely",
        "tools": [
            "execute_python_code",
            "execute_python_expression", 
            "install_package",
            "list_installed_packages",
            "check_syntax"
        ],
        "python_version": sys.version,
        "python_executable": sys.executable
    }, indent=2)

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