Skip to main content

Stdio transport adapter for mcpbytes-lambda MCP servers

Project description

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.

๐Ÿ”— Related Packages

๐Ÿ“š Documentation


Built with โค๏ธ for the MCP ecosystem

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mcpbytes_lambda_stdio-0.1.0.tar.gz (23.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mcpbytes_lambda_stdio-0.1.0-py3-none-any.whl (7.2 kB view details)

Uploaded Python 3

File details

Details for the file mcpbytes_lambda_stdio-0.1.0.tar.gz.

File metadata

  • Download URL: mcpbytes_lambda_stdio-0.1.0.tar.gz
  • Upload date:
  • Size: 23.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for mcpbytes_lambda_stdio-0.1.0.tar.gz
Algorithm Hash digest
SHA256 1b147d94dd8de2723b981103282965d0e2e83cab97338dd6c260c1ffe4288c43
MD5 487642c678520bdc6d2d594e9dd0d366
BLAKE2b-256 2a34a06802401d69184b45ed92052101a657968c7239f274eafdea76fd57c022

See more details on using hashes here.

File details

Details for the file mcpbytes_lambda_stdio-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for mcpbytes_lambda_stdio-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a465c9040e4566ee738331ee8fbdc683aacf4a4d646313b32eb792ec9f680cd4
MD5 3105fbefa5e003fa3378a8d560c7fca2
BLAKE2b-256 b1977edb019771a9f5494e09e01770a9623cea4ac920752f6c1d9e428ce98962

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page