# 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())
```