Thanks to visit codestin.com
Credit goes to corv89.github.io

Skip to content

MCP Server Module

Model Context Protocol (MCP) server implementation for LLM integration with PyPy sandbox.

Overview

The MCP server module (v0.5.0) implements a Model Context Protocol server that exposes Shannot's PyPy sandbox capabilities to LLM clients like Claude Desktop and Claude Code. It provides script execution with hybrid approval workflow and profile-based security.

Key Components:

  • ShannotMCPServer - Main MCP server implementing PyPy sandbox integration
  • MCPServer - Base server class with JSON-RPC request routing
  • MCP Protocol - Pure stdlib JSON-RPC 2.0 over stdin/stdout
  • MCP Types - Dataclass definitions for tools, resources, content

Architecture

shannot.mcp/
├── __init__.py          # Package exports
├── protocol.py          # JSON-RPC transport (stdlib only)
├── types.py             # MCP type definitions
├── server.py            # Base MCPServer class
└── server_impl.py       # ShannotMCPServer implementation

MCP Tools

sandbox_run

Execute Python 3.6 script in PyPy sandbox with profile-based approval.

Input Schema:

{
  "script": str,        # Python 3.6 code to execute
  "profile": str,       # "minimal", "readonly", "diagnostics" (default: "minimal")
  "name": str           # Optional session name for tracking
}

Returns:

{
  "status": "success" | "pending_approval" | "denied" | "error",
  "exit_code": int,            # (if success)
  "stdout": str,               # (if success)
  "stderr": str,               # (if success)
  "duration": float,           # (if success)
  "session_id": str,           # (if pending_approval)
  "instructions": list[str],   # (if pending_approval)
  "reason": str,               # (if denied)
  "error": str                 # (if error)
}

Example:

# LLM calls tool
sandbox_run({
  "script": "import subprocess\nsubprocess.call(['df', '-h'])",
  "profile": "diagnostics"
})

# Fast path response (auto-approved):
{
  "status": "success",
  "exit_code": 0,
  "stdout": "Filesystem      Size  Used Avail Use% Mounted on\n...",
  "stderr": "",
  "duration": 0.123,
  "profile": "diagnostics"
}

# Review path response (needs approval):
{
  "status": "pending_approval",
  "session_id": "20251222-mcp-request-a3f2",
  "detected_operations": ["curl https://example.com"],
  "instructions": [
    "Review with: shannot approve show 20251222-mcp-request-a3f2",
    "Approve and execute: shannot approve --execute 20251222-mcp-request-a3f2"
  ]
}

session_result

Poll status of pending session created by sandbox_run.

Input Schema:

{
  "session_id": str  # Session ID from sandbox_run
}

Returns:

{
  "session_id": str,
  "status": "pending" | "executed" | "expired" | "cancelled" | "rejected" | "failed",
  "created_at": str,
  "exit_code": int,       # (if executed)
  "stdout": str,          # (if executed)
  "stderr": str,          # (if executed)
  "executed_at": str,     # (if executed)
  "error": str,           # (if failed)
  "expires_at": str,      # (if pending)
  "instructions": list,   # (if pending)
  "message": str          # (if expired/cancelled/rejected)
}

Example:

# Poll session
session_result({"session_id": "20251222-mcp-request-a3f2"})

# Response (after user approval):
{
  "session_id": "20251222-mcp-request-a3f2",
  "status": "executed",
  "exit_code": 0,
  "stdout": "...",
  "stderr": "",
  "executed_at": "2025-12-22T14:30:45"
}

MCP Resources

sandbox://profiles

List available approval profiles.

Returns: JSON array of profile names

["minimal", "readonly", "diagnostics"]

sandbox://profiles/{name}

Get profile configuration.

Returns: Profile structure

{
  "auto_approve": ["ls", "cat", "grep", "find"],
  "always_deny": ["rm -rf /", "dd if=/dev/zero", ":(){ :|:& };:"]
}

sandbox://status

Runtime status and configuration.

Returns: Status object

{
  "version": "0.5.1",
  "runtime_available": true,
  "profiles": ["minimal", "readonly", "diagnostics"],
  "runtime": {
    "pypy_sandbox": "/path/to/pypy-sandbox",
    "lib_python": "/path/to/lib-python/3",
    "lib_pypy": "/path/to/lib_pypy"
  }
}

Server Configuration

Creating a Server

from pathlib import Path
from shannot.mcp.server_impl import ShannotMCPServer
from shannot.mcp.protocol import serve

# Create server with default profiles
server = ShannotMCPServer(
    profile_paths=None,  # Use defaults
    verbose=False
)

# Create server with custom profiles
server = ShannotMCPServer(
    profile_paths=[
        Path("~/.config/shannot/custom.json").expanduser()
    ],
    verbose=True
)

# Start serving (blocks)
serve(server.handle_request)

Profile Structure

Custom profiles are JSON files with:

{
  "auto_approve": [
    "ls", "cat", "grep", "find"
  ],
  "always_deny": [
    "rm -rf /",
    "dd if=/dev/zero"
  ]
}

Place in ~/.config/shannot/{name}.json and server auto-discovers.

Installation for LLM Clients

Claude Desktop (macOS)

shannot setup mcp install
# or
shannot setup mcp install --client claude-desktop

Adds to ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "shannot": {
      "command": "shannot-mcp",
      "args": [],
      "env": {}
    }
  }
}

Claude Code

shannot setup mcp install --client claude-code

Generates .mcp.json snippet or updates user config.

Manual Configuration

{
  "mcpServers": {
    "shannot": {
      "command": "shannot-mcp",
      "args": ["--verbose"],  # optional
      "env": {}
    }
  }
}

Execution Workflow

Three Execution Paths

1. Fast Path (Auto-Approved) - AST detects only allowed operations - Executes immediately in PyPy sandbox - Returns results directly

2. Review Path (Pending Approval) - AST detects unapproved operations - Creates session for user review - User approves with shannot approve - LLM polls with session_result

3. Blocked Path (Denied) - AST detects denied operations - Rejects immediately - Returns error with reason

AST Analysis

Server performs best-effort AST analysis to detect subprocess calls:

# Detected (literal arguments):
import subprocess
subprocess.call(['ls', '/tmp'])  # → "ls /tmp"

# Not detected (dynamic arguments):
import subprocess
cmd = ['ls', '/tmp']
subprocess.call(cmd)  # → []

Important: AST analysis is UX optimization, NOT security. Security enforced at runtime by PyPy sandbox subprocess virtualization.

Security Model

Protection Layers

  1. PyPy Sandbox: Syscall-level virtualization
  2. Subprocess Virtualization: Profile-based approval enforcement
  3. Session Workflow: Interactive review for sensitive operations
  4. Profile Allowlists: Restrict executable commands
  5. Session TTL: 1-hour expiry for pending approvals

What's Protected

✅ Unauthorized file modifications (sandbox prevents writes) ✅ Network access (socket operations virtualized) ✅ Privilege escalation (no actual syscalls reach kernel) ✅ Subprocess injection (profiles enforce allowlist)

Limitations

⚠️ AST analysis can miss dynamic operations ⚠️ Python 3.6 syntax only (PyPy sandbox limitation) ⚠️ Session approval required for non-trivial operations

Programmatic Usage

Synchronous Server

from shannot.mcp.server_impl import ShannotMCPServer
from shannot.mcp.protocol import serve

# Create and serve
server = ShannotMCPServer()
serve(server.handle_request)

Custom Handler

# Process single request
request = {
    "jsonrpc": "2.0",
    "method": "tools/list",
    "id": 1
}
response = server.handle_request(request)
print(response)

Testing

# Test tool call
result = server._handle_sandbox_run({
    "script": "print('hello')",
    "profile": "minimal"
})
print(result.text)  # JSON response

CLI Usage

# Start server (default profiles)
shannot-mcp

# With verbose logging
shannot-mcp --verbose

# Load custom profile
# (place profile at ~/.config/shannot/custom.json)
shannot-mcp --profile custom

# With multiple profiles
shannot-mcp --profile ~/.config/shannot/profile1.json \
            --profile ~/.config/shannot/profile2.json

Error Handling

# Runtime not found
{
  "status": "error",
  "error": "PyPy sandbox runtime not found. Run 'shannot setup runtime' to install."
}

# Invalid profile
{
  "status": "error",
  "error": "Unknown profile 'nonexistent'"
}

# Session not found
{
  "status": "error",
  "error": "Session not found: invalid-id"
}

API Reference

shannot.mcp.server_impl

  • ShannotMCPServer - Main MCP server class
  • find_runtime() - Locate PyPy sandbox runtime

shannot.mcp.server

  • MCPServer - Base server with request routing
  • ServerCapabilities - Capability negotiation
  • ServerInfo - Server metadata

shannot.mcp.protocol

  • read_message() - Read JSON-RPC message from stdin
  • write_message() - Write JSON-RPC message to stdout
  • serve() - Main serving loop

shannot.mcp.types

  • TextContent - Text content type
  • Tool - Tool definition
  • Resource - Resource definition
  • ServerInfo - Server information