Skip to main content

mcpbytes-lambda-stdio

Python 3.12+ MCP 2025-06-18 License: Apache 2.0

Ultra-minimal stdio transport adapter for Model Context Protocol (MCP) servers. Enables local MCP server deployment with line-delimited JSON-RPC over stdin/stdout for Claude Desktop, VS Code MCP, and other MCP clients.

🚀 Quick Start

import sys
from mcpbytes_lambda.core import MCPServer
from mcpbytes_lambda.stdio import StdioAdapter

# Create your MCP server with tools
mcp = MCPServer(name="local-server", version="1.0.0")

@mcp.tool(name="hello", description="Say hello")
def hello(name: str) -> str:
    return f"Hello, {name}!"

def main():
    """Local MCP server mode - stdio transport."""
    adapter = StdioAdapter()
    
    try:
        while True:
            # Read JSON-RPC line from stdin
            line = sys.stdin.readline()
            if not line:
                break
                
            # Process: stdin → JSON-RPC → tool execution → JSON-RPC → stdout
            response_json = mcp.handle(line, adapter)
            
            # Write response to stdout
            sys.stdout.write(response_json)
            sys.stdout.flush()
            
    except (KeyboardInterrupt, EOFError):
        sys.exit(0)

if __name__ == "__main__":
    main()

Configure in Claude Desktop:

{
  "mcpServers": {
    "local-server": {
      "command": "python3",
      "args": ["/path/to/your/server.py"]
    }
  }
}

✨ Features

  • 📺 Line-Delimited JSON-RPC - Standard MCP stdio transport protocol
  • 🏠 Local Development - Perfect for Claude Desktop and local MCP clients
  • ⚡ Zero Overhead - Minimal transport layer with direct JSON passthrough
  • 🔄 Dual-Mode Support - Same tools work in both Lambda and local environments
  • 🧪 Easy Testing - Simple command-line testing with JSON inputs
  • 📊 Structured Output - Full MCP 2025-06-18 compliance
  • 🔧 Developer-Friendly - Seamless local development workflow

📋 Requirements

  • Python 3.12+
  • mcpbytes-lambda-core package
  • Standard input/output streams (terminal, process pipes)

🏗️ Architecture

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   MCP Client    │───▶│ Stdio Adapter   │───▶│   MCP Core      │
│ (Claude Desktop)│    │ (stdin/stdout)  │    │   JSON-RPC      │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         │                       ▼                       │
         │              ┌─────────────────┐              │
         │              │ Line-Delimited  │              │
         │              │   JSON-RPC      │              │
         │              │  .strip() only  │              │
         │              └─────────────────┘              │
         │                                                │
         ▼                                                ▼
┌─────────────────┐                              ┌─────────────────┐
│   MCP Client    │◀─────────────────────────────│   Tool Results  │
│   Integration   │                              │   JSON-RPC      │
└─────────────────┘                              └─────────────────┘

🛠️ Usage Examples

Basic Stdio Server

import sys
from mcpbytes_lambda.core import MCPServer, ToolResult, TextContent
from mcpbytes_lambda.stdio import StdioAdapter

mcp = MCPServer(name="my-tools", version="1.0.0")

@mcp.tool(name="file.read")
def read_file(path: str) -> ToolResult:
    """Read a text file."""
    try:
        with open(path, 'r') as f:
            content = f.read()
        return ToolResult(
            content=[TextContent(text=content)],
            isError=False
        )
    except Exception as e:
        return ToolResult(
            content=[TextContent(text=f"Error reading file: {e}")],
            isError=True
        )

def main():
    adapter = StdioAdapter()
    
    while True:
        try:
            line = sys.stdin.readline()
            if not line:
                break
                
            response = mcp.handle(line, adapter)
            sys.stdout.write(response)
            sys.stdout.flush()
            
        except (KeyboardInterrupt, EOFError):
            break

if __name__ == "__main__":
    main()

Dual-Mode Server (Lambda + Stdio)

import sys
from typing import Dict, Any
from mcpbytes_lambda.core import MCPServer
from mcpbytes_lambda.stdio import StdioAdapter
from mcpbytes_lambda.apigw import ApiGatewayAdapter

# Shared MCP server instance
mcp = MCPServer(name="dual-mode-server", version="1.0.0")

@mcp.tool(name="calculate")
def calculate(expression: str) -> str:
    """Safely evaluate mathematical expressions."""
    try:
        # Basic calculator logic here
        result = eval(expression.replace('^', '**'))  # Simple example
        return f"{expression} = {result}"
    except Exception as e:
        return f"Error: {e}"

def main():
    """Local stdio mode."""
    print("Starting MCP Server (stdio mode)", file=sys.stderr)
    adapter = StdioAdapter()
    
    try:
        while True:
            line = sys.stdin.readline()
            if not line:
                break
                
            response = mcp.handle(line, adapter)
            sys.stdout.write(response)
            sys.stdout.flush()
            
    except (KeyboardInterrupt, EOFError):
        print("Shutting down MCP Server", file=sys.stderr)

def lambda_handler(event: Dict[str, Any], context) -> Dict[str, Any]:
    """AWS Lambda mode."""
    adapter = ApiGatewayAdapter()
    
    if adapter.need_preflight(event):
        return adapter.preflight_response(event.get("headers"))
    
    response = mcp.handle(event, adapter, headers=event.get("headers"))
    return adapter.add_cors(response, event.get("headers"))

if __name__ == "__main__":
    main()

With Logging and Error Handling

import sys
import logging
from mcpbytes_lambda.core import MCPServer
from mcpbytes_lambda.stdio import StdioAdapter

# Configure logging to stderr (stdout reserved for MCP protocol)
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    stream=sys.stderr
)
logger = logging.getLogger(__name__)

mcp = MCPServer(name="robust-server", version="1.0.0")

@mcp.tool(name="system.info")
def system_info() -> Dict[str, str]:
    """Get system information."""
    import platform
    import os
    
    return {
        "platform": platform.system(),
        "python_version": platform.python_version(),
        "cwd": os.getcwd(),
        "pid": os.getpid()
    }

def main():
    logger.info("Starting MCP Server (stdio mode)")
    adapter = StdioAdapter()
    
    try:
        while True:
            line = sys.stdin.readline()
            if not line:
                logger.info("EOF received, shutting down")
                break
                
            line = line.strip()
            if not line:
                continue
                
            logger.debug(f"Processing request: {line[:100]}...")
            
            try:
                response = mcp.handle(line, adapter)
                sys.stdout.write(response)
                sys.stdout.flush()
                logger.debug("Response sent successfully")
                
            except Exception as e:
                logger.error(f"Request processing failed: {e}", exc_info=True)
                # Send error response
                error_response = {
                    "jsonrpc": "2.0",
                    "id": None,
                    "error": {"code": -32603, "message": "Internal error"}
                }
                sys.stdout.write(json.dumps(error_response) + '\n')
                sys.stdout.flush()
                
    except (KeyboardInterrupt, EOFError):
        logger.info("Shutting down MCP Server")
    except Exception as e:
        logger.error(f"Fatal error: {e}", exc_info=True)
        sys.exit(1)

if __name__ == "__main__":
    main()

🖥️ Client Configuration

Claude Desktop

Add to ~/.config/claude_desktop_config.json (macOS/Linux) or %AppData%\Claude\claude_desktop_config.json (Windows):

{
  "mcpServers": {
    "my-mcp-server": {
      "command": "python3",
      "args": ["/full/path/to/your/server.py"],
      "env": {
        "PYTHONPATH": "/path/to/your/packages"
      }
    }
  }
}

With virtual environment:

{
  "mcpServers": {
    "my-server": {
      "command": "/path/to/venv/bin/python",
      "args": ["/path/to/server.py"]
    }
  }
}

Using uv:

{
  "mcpServers": {
    "my-server": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/project", "python", "server.py"]
    }
  }
}

VS Code MCP Extension

Add to .vscode/mcp.json:

{
  "servers": {
    "my-mcp-server": {
      "command": "python3",
      "args": ["/path/to/server.py"],
      "cwd": "/path/to/project"
    }
  }
}

Custom MCP Client

import subprocess
import json

# Start MCP server process
process = subprocess.Popen(
    ["python3", "server.py"],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    text=True
)

# Send initialization
request = {
    "jsonrpc": "2.0",
    "method": "initialize",
    "id": "init-1"
}

process.stdin.write(json.dumps(request) + '\n')
process.stdin.flush()

# Read response
response = process.stdout.readline()
result = json.loads(response)
print("Initialize result:", result)

# List tools
request = {
    "jsonrpc": "2.0",
    "method": "tools/list",
    "id": "list-1"
}

process.stdin.write(json.dumps(request) + '\n')
process.stdin.flush()

response = process.stdout.readline()
tools = json.loads(response)
print("Available tools:", tools)

# Clean shutdown
process.stdin.close()
process.wait()

🧪 Testing

Command Line Testing

Create test_requests.jsonl:

{"jsonrpc": "2.0", "method": "initialize", "id": "1"}
{"jsonrpc": "2.0", "method": "tools/list", "id": "2"}
{"jsonrpc": "2.0", "method": "tools/call", "params": {"name": "hello", "arguments": {"name": "World"}}, "id": "3"}

Test your server:

python3 server.py < test_requests.jsonl

Interactive Testing

# Start server
python3 server.py

# Type JSON-RPC requests (press Enter after each):
{"jsonrpc": "2.0", "method": "initialize", "id": "1"}
{"jsonrpc": "2.0", "method": "tools/list", "id": "2"}
{"jsonrpc": "2.0", "method": "tools/call", "params": {"name": "my.tool", "arguments": {"param": "value"}}, "id": "3"}

# Press Ctrl+C or Ctrl+D to exit

Unit Tests

import io
import sys
from contextlib import redirect_stdout, redirect_stdin

def test_stdio_adapter():
    from mcpbytes_lambda.stdio import StdioAdapter
    
    adapter = StdioAdapter()
    
    # Test line processing
    line = '{"jsonrpc": "2.0", "method": "initialize", "id": "1"}\n'
    core_request = adapter.to_core_request(line)
    assert core_request == '{"jsonrpc": "2.0", "method": "initialize", "id": "1"}'
    
    # Test response formatting
    response = {"jsonrpc": "2.0", "id": "1", "result": {"test": True}}
    output = adapter.from_core_response(response)
    assert output.endswith('\n')
    assert '"test": true' in output.lower()

def test_server_integration():
    # Mock stdin/stdout for testing
    input_data = '{"jsonrpc": "2.0", "method": "initialize", "id": "test"}\n'
    
    with redirect_stdin(io.StringIO(input_data)):
        with redirect_stdout(io.StringIO()) as output:
            # Run one iteration of server loop
            try:
                line = sys.stdin.readline()
                if line:
                    response = mcp.handle(line, StdioAdapter())
                    sys.stdout.write(response)
            except EOFError:
                pass
    
    result = output.getvalue()
    assert '"jsonrpc": "2.0"' in result
    assert '"id": "test"' in result

Integration Testing with Process

import subprocess
import json
import time

def test_mcp_server_process():
    # Start server process
    process = subprocess.Popen(
        ["python3", "server.py"],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True
    )
    
    try:
        # Test initialize
        init_request = {
            "jsonrpc": "2.0",
            "method": "initialize",
            "id": "test-init"
        }
        
        process.stdin.write(json.dumps(init_request) + '\n')
        process.stdin.flush()
        
        # Read response with timeout
        response_line = process.stdout.readline()
        assert response_line, "No response received"
        
        response = json.loads(response_line)
        assert response["jsonrpc"] == "2.0"
        assert response["id"] == "test-init"
        assert "result" in response
        
    finally:
        process.terminate()
        process.wait(timeout=5)

🚀 Development Workflow

Project Structure

my-mcp-server/
├── server.py          # Main server file
├── requirements.txt   # Dependencies
├── test_requests.jsonl # Test cases
├── .env              # Environment variables
├── README.md         # Documentation
└── tests/
    ├── __init__.py
    ├── test_tools.py
    └── test_server.py

Development Script

#!/usr/bin/env python3
"""Development helper for MCP server."""

import sys
import json
import argparse
from pathlib import Path

def create_test_request(method: str, params: dict = None, id_val: str = "1") -> str:
    """Create a JSON-RPC test request."""
    request = {
        "jsonrpc": "2.0",
        "method": method,
        "id": id_val
    }
    if params:
        request["params"] = params
    return json.dumps(request)

def main():
    parser = argparse.ArgumentParser(description="MCP Server Development Helper")
    parser.add_argument("--test", action="store_true", help="Run test requests")
    parser.add_argument("--method", help="Test specific method")
    parser.add_argument("--params", help="JSON parameters for method")
    
    args = parser.parse_args()
    
    if args.test:
        # Generate common test requests
        requests = [
            create_test_request("initialize", id_val="init-1"),
            create_test_request("tools/list", id_val="list-1"),
        ]
        
        if args.method:
            params = json.loads(args.params) if args.params else {}
            requests.append(create_test_request(args.method, params, "custom-1"))
        
        for req in requests:
            print(req)
    else:
        # Run the actual server
        from server import main as server_main
        server_main()

if __name__ == "__main__":
    main()

Debugging Tips

  1. Use stderr for logging (stdout is reserved for MCP protocol)
  2. Test with simple JSON files before integrating with clients
  3. Validate JSON-RPC format - clients are strict about compliance
  4. Handle EOF gracefully - clients may close stdin unexpectedly
  5. Flush stdout after each response to ensure delivery

📊 Performance Considerations

Memory Usage

  • Minimal overhead - No HTTP parsing or transport layers
  • Streaming processing - Handle one request at a time
  • No buffering - Direct stdin/stdout interaction

Latency

  • Near-zero transport overhead - Simple string operations
  • Direct JSON processing - No intermediate representations
  • Immediate response - No batching or queuing

Reliability

  • Process isolation - Each client gets its own server process
  • Automatic cleanup - Process termination cleans up resources
  • Simple error handling - Failures are contained to single requests

🤝 Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature-name
  3. Make your changes and add tests
  4. Run tests: python -m pytest
  5. Test with actual MCP clients (Claude Desktop, etc.)
  6. Submit a pull request

📄 License

Apache 2.0 License - see LICENSE for details.

📚 Documentation


Built with ❤️ for the MCP ecosystem

Metadata

Release files for mcpbytes-lambda-stdio 0.1.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mcpbytes-lambda-stdio 0.1.3
File Size Uploaded Provenance
mcpbytes_lambda_stdio-0.1.3.tar.gz 9.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for mcpbytes-lambda-stdio 0.1.3
File Interpreter ABI Platform
mcpbytes_lambda_stdio-0.1.3-py3-none-any.whl Python 3 none any Details

Total release size: 19.3 kB

Release files / mcpbytes_lambda_stdio-0.1.3.tar.gz

Download URL mcpbytes_lambda_stdio-0.1.3.tar.gz
Size 9.2 kB
Tags Source
SHA-256 checksum
How to use checksums
0cff2d88ecf63372bfdc162cc48040f1af2ecf2a9f5b77a0a6ac048bf7ed5f96
BLAKE2b-256 checksum
How to use checksums
7058d86d2156f08ec66be7cb4b110111ad5f63343f83ac6fcf1588d26af123ac
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.7

Release files / mcpbytes_lambda_stdio-0.1.3-py3-none-any.whl

Download URL mcpbytes_lambda_stdio-0.1.3-py3-none-any.whl
Size 10.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
72521febb096efaa5d0b85ed82f09844b9cbc911c6ae0b819bbdcb78d00cd988
BLAKE2b-256 checksum
How to use checksums
034405eb7f76932dd98b4739aee1d088d54b15f9c821991d0120daa172f683c0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.7

Security for mcpbytes-lambda-stdio 0.1.3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page