#
tokens: 1878/50000 6/6 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── .python-version
├── pyproject.toml
├── README.md
├── src
│   └── python_local
│       ├── __init__.py
│       └── server.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
3.12

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info

# Virtual environments
.venv

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# python_local MCP Server

An MCP Server that provides an interactive Python REPL (Read-Eval-Print Loop) environment.

## Components

### Resources

The server provides access to REPL session history:
- Custom `repl://` URI scheme for accessing session history
- Each session's history can be viewed as a text/plain resource
- History shows input code and corresponding output for each execution

### Tools

The server implements one tool:
- `python_repl`: Executes Python code in a persistent session
  - Takes `code` (Python code to execute) and `session_id` as required arguments
  - Maintains separate state for each session
  - Supports both expressions and statements
  - Captures and returns stdout/stderr output

## Configuration

### Install

#### Claude Desktop

On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
On Windows: `%APPDATA%/Claude/claude_desktop_config.json`

<details>
  <summary>Development/Unpublished Servers Configuration</summary>
  ```json
  "mcpServers": {
    "python_local": {
      "command": "uv",
      "args": [
        "--directory",
        "/path/to/python_local",
        "run",
        "python_local"
      ]
    }
  }
  ```
</details>

<details>
  <summary>Published Servers Configuration</summary>
  ```json
  "mcpServers": {
    "python_local": {
      "command": "uvx",
      "args": [
        "python_local"
      ]
    }
  }
  ```
</details>

## Development

### Building and Publishing

To prepare the package for distribution:

1. Sync dependencies and update lockfile:
```bash
uv sync
```

2. Build package distributions:
```bash
uv build
```

This will create source and wheel distributions in the `dist/` directory.

3. Publish to PyPI:
```bash
uv publish
```

Note: You'll need to set PyPI credentials via environment variables or command flags:
- Token: `--token` or `UV_PUBLISH_TOKEN`
- Or username/password: `--username`/`UV_PUBLISH_USERNAME` and `--password`/`UV_PUBLISH_PASSWORD`

### Debugging

Since MCP servers run over stdio, debugging can be challenging. For the best debugging
experience, we strongly recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector).

You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with this command:

```bash
npx @modelcontextprotocol/inspector uv --directory /path/to/python_local run python-local
```

Upon launching, the Inspector will display a URL that you can access in your browser to begin debugging.
```

--------------------------------------------------------------------------------
/src/python_local/__init__.py:
--------------------------------------------------------------------------------

```python
from . import server
import asyncio

def main():
    """Main entry point for the package."""
    asyncio.run(server.main())

# Optionally expose other important items at package level
__all__ = ['main', 'server']
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
[project]
name = "python-local"
version = "0.1.0"
description = "An MCP Server to run python locally"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [ "mcp>=1.1.0",]
[[project.authors]]
name = "Alex Velikanov"
email = "[email protected]"

[build-system]
requires = [ "hatchling",]
build-backend = "hatchling.build"

[project.scripts]
python-local = "python_local:main"

```

--------------------------------------------------------------------------------
/src/python_local/server.py:
--------------------------------------------------------------------------------

```python
import asyncio
import sys
from io import StringIO
import traceback
import builtins

from mcp.server.models import InitializationOptions
import mcp.types as types
from mcp.server import NotificationOptions, Server
from pydantic import AnyUrl
import mcp.server.stdio

server = Server("python_interpreter")

# Global state for REPL sessions
class ReplSession:
    def __init__(self):
        self.locals = {"__builtins__": builtins}
        self.history = []
        
    def execute(self, code: str) -> tuple[str, str]:
        stdout_capture = StringIO()
        stderr_capture = StringIO()
        
        old_stdout, old_stderr = sys.stdout, sys.stderr
        sys.stdout, sys.stderr = stdout_capture, stderr_capture
        
        try:
            # Try to evaluate as expression first
            try:
                result = eval(code, self.locals)
                if result is not None:
                    print(repr(result))
            except SyntaxError:
                # If not an expression, execute as statement
                exec(code, self.locals)
                
        except Exception:
            traceback.print_exc()
        finally:
            sys.stdout, sys.stderr = old_stdout, old_stderr
            
        return stdout_capture.getvalue(), stderr_capture.getvalue()

sessions: dict[str, ReplSession] = {}

@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
    return [
        types.Tool(
            name="python_repl",
            description="Interactive Python REPL that maintains session state. NOTE THE USER DOES NOT SEE THE STDOUT/STDERR OUTPUT, MAKE SURE TO FORMAT/SUMMARIZEc IT APPROPRIATELY IN YOUR RESPONSE TO THE USER",
            inputSchema={
                "type": "object",
                "properties": {
                    "code": {"type": "string", "description": "Python code to execute"},
                    "session_id": {"type": "string", "description": "Session identifier"},
                },
                "required": ["code", "session_id"],
            },
        )
    ]

@server.call_tool()
async def handle_call_tool(
    name: str, 
    arguments: dict | None
) -> list[types.TextContent]:
    if name != "python_repl":
        raise ValueError(f"Unknown tool: {name}")

    if not arguments or "code" not in arguments or "session_id" not in arguments:
        raise ValueError("Missing code or session_id argument")

    code = arguments["code"]
    session_id = arguments["session_id"]
    
    if session_id not in sessions:
        sessions[session_id] = ReplSession()
    
    session = sessions[session_id]
    stdout, stderr = session.execute(code)
    session.history.append({"code": code, "stdout": stdout, "stderr": stderr})
    
    return [types.TextContent(type="text", text=f"NOTE THE USER DOES NOT SEE THIS OUTPUT, MAKE SURE TO FORMAT IT APPROPRIATELY IN YOUR RESPONSE TO THE USER\n\n{stdout}{stderr}")]

@server.list_resources()
async def handle_list_resources() -> list[types.Resource]:
    return [
        types.Resource(
            uri=AnyUrl(f"repl://{session_id}/history"),
            name=f"REPL Session {session_id}",
            description="REPL session history",
            mimeType="text/plain",
        )
        for session_id in sessions
    ]

@server.read_resource()
async def handle_read_resource(uri: AnyUrl) -> str:
    if uri.scheme != "repl":
        raise ValueError(f"Unsupported URI scheme: {uri.scheme}")

    session_id = uri.host
    if session_id not in sessions:
        raise ValueError(f"Session not found: {session_id}")
        
    history = sessions[session_id].history
    return "\n\n".join(
        f"In [{i}]: {entry['code']}\n"
        f"Out[{i}]:\n{entry['stdout']}{entry['stderr']}"
        for i, entry in enumerate(history)
    )

async def main():
    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            InitializationOptions(
                server_name="python_interpreter",
                server_version="0.1.0",
                capabilities=server.get_capabilities(
                    notification_options=NotificationOptions(),
                    experimental_capabilities={},
                ),
            ),
        )

if __name__ == "__main__":
    asyncio.run(main())
```