Python SDK for building Model Context Protocol (MCP) tools that compile to WebAssembly.
pip install ftl-sdkpip install ftl-sdk==0.1.0pip install git+https://github.com/fastertools/ftl.git#subdirectory=sdk/pythonpip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ ftl-sdkThis SDK provides:
- Decorator-based API for easy tool creation
- Automatic JSON Schema generation from type hints
- Support for both sync and async functions
- Automatic return value conversion to MCP format
- Zero-dependency implementation (only requires
spin-sdk) - Full compatibility with Spin WebAssembly components
- Seamless deployment to Fermyon Cloud
- Python 3.10 or later
componentize-pyfor building WebAssembly componentsspin-sdkfor Spin runtime integration
from ftl_sdk import FTL
# Create FTL application instance
ftl = FTL()
@ftl.tool
def echo(message: str) -> str:
"""Echo back the input."""
return f"Echo: {message}"
# Create the Spin handler
Handler = ftl.create_handler()# Create virtual environment
python3 -m venv venv
source venv/bin/activate
# Install dependencies
pip install componentize-py spin-sdk ftl-sdk
# Build
ftl build
# Deploy to Fermyon Cloud
ftl deployftl = FTL()Creates an FTL application instance for registering tools.
@ftl.tool
def my_tool(param: str) -> str:
"""Tool description."""
return result
# With custom name and annotations
@ftl.tool(name="custom_name", annotations={"priority": "high"})
def another_tool(data: dict) -> dict:
return {"processed": data}The decorator:
- Automatically generates JSON Schema from type hints
- Extracts docstring as tool description
- Handles both sync and async functions
- Validates output against return type annotation
Creates a Spin HTTP handler that:
- Returns tool metadata on GET / requests
- Routes to specific tools on POST /{tool_name} requests
- Handles async/await for async tool functions
- Automatically converts return values to MCP format
# Simple text response
ToolResponse.text("Hello, world!")
# Error response
ToolResponse.error("Something went wrong")
# Response with structured content
ToolResponse.with_structured("Operation complete", {"result": 42})For advanced response handling, you can use the ToolResult class:
from ftl_sdk import ToolResult, ToolContent
# Create custom response with multiple content items
result = ToolResult(
content=[
ToolContent.text("Processing complete"),
ToolContent.text("Results:", {"priority": 0.8})
],
structured_content={"status": "success", "count": 5}
)
# Error result
error_result = ToolResult.error("Operation failed: invalid input")
# Text result (shorthand)
text_result = ToolResult.text("Simple text response")# Text content
ToolContent.text("Some text", {"priority": 0.8})
# Image content
ToolContent.image(base64_data, "image/png")
# Audio content
ToolContent.audio(base64_data, "audio/wav")
# Resource reference
ToolContent.resource({"uri": "file:///example.txt"})# Check content types
if is_text_content(content):
print(content["text"])from ftl_sdk import FTL
ftl = FTL()
@ftl.tool
def echo(message: str) -> str:
"""Echo the input."""
return f"Echo: {message}"
@ftl.tool
def reverse_text(text: str) -> str:
"""Reverse the input text."""
return text[::-1]
@ftl.tool
def word_count(text: str) -> dict:
"""Count words in text."""
count = len(text.split())
return {"text": text, "word_count": count}
Handler = ftl.create_handler()import asyncio
from ftl_sdk import FTL
ftl = FTL()
@ftl.tool
async def fetch_data(url: str) -> dict:
"""Fetch data from URL asynchronously."""
# Simulate async HTTP request
await asyncio.sleep(0.1)
return {
"url": url,
"status": "success",
"data": {"example": "data"}
}
@ftl.tool
async def process_items(items: list[str]) -> dict:
"""Process items with async operations."""
results = []
for item in items:
# Simulate async processing
await asyncio.sleep(0.01)
results.append(item.upper())
return {
"original": items,
"processed": results,
"count": len(results)
}
# Mix sync and async tools
@ftl.tool
def sync_add(a: int, b: int) -> int:
"""Add two numbers synchronously."""
return a + b
Handler = ftl.create_handler()from ftl_sdk import FTL
ftl = FTL()
@ftl.tool
def divide(a: float, b: float) -> float:
"""Divide two numbers."""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# Async error handling
@ftl.tool
async def validate_data(data: dict) -> dict:
"""Validate data asynchronously."""
if "required_field" not in data:
raise KeyError("Missing required field: required_field")
# Simulate async validation
await asyncio.sleep(0.05)
if not isinstance(data["required_field"], str):
raise TypeError("required_field must be a string")
return {"status": "valid", "data": data}
Handler = ftl.create_handler()The FTL framework automatically catches exceptions and returns them as error responses.
Tools must be compiled to WebAssembly to run on Spin:
-
Install dependencies:
pip install componentize-py spin-sdk ftl-sdk
-
Build with componentize-py:
componentize-py -w spin-http componentize app -o app.wasm
-
Or use FTL CLIs build command:
ftl build
Always use type hints for better code clarity and automatic schema generation:
from ftl_sdk import FTL
ftl = FTL()
@ftl.tool
def my_handler(message: str) -> str:
"""Handle a message with automatic type validation."""
return f"Received: {message}"The FTL framework automatically catches exceptions and converts them to error responses. Use type hints for automatic validation:
from ftl_sdk import FTL
ftl = FTL()
@ftl.tool
def safe_handler(required_field: str, optional_value: int = 10) -> dict:
"""Process data with automatic validation and error handling."""
# Type validation is automatic - required_field is guaranteed to be a string
# Framework automatically catches and converts exceptions to error responses
if not required_field.strip():
raise ValueError("required_field cannot be empty")
# Process the validated input
result = len(required_field) * optional_value
return {
"processed": required_field.upper(),
"result": result,
"status": "success"
}Write comprehensive tests for your decorated tools:
import pytest
from ftl_sdk import FTL
# Your tool module
ftl = FTL()
@ftl.tool
def echo_message(message: str) -> str:
"""Echo back the input message."""
if not message:
raise ValueError("Message cannot be empty")
return f"Echo: {message}"
# Test the tool function directly
def test_echo_success():
result = echo_message("test")
assert result == "Echo: test"
def test_echo_validation():
with pytest.raises(ValueError, match="Message cannot be empty"):
echo_message("")
# Test the HTTP handler integration
def test_handler_integration():
handler = ftl.create_handler()
# Test metadata endpoint
metadata = handler.get_tools_metadata()
assert len(metadata) == 1
assert metadata[0]["name"] == "echo_message"-
Python Version: Requires Python 3.10 or later. Python 3.11+ recommended.
-
Zero Dependencies: This SDK has no external dependencies beyond
spin-sdk, keeping the WASM bundle size minimal. -
Input Validation: The FTL gateway handles input validation against your JSON Schema. Your handler can assume inputs are valid.
-
Virtual Environments: Always use a virtual environment to ensure consistent builds.
-
WASM Size: Python WASM components are larger than TypeScript/Rust equivalents (~37MB), but this is acceptable for cloud deployment.
-
Type Safety: Use type hints and mypy for better code quality and fewer runtime errors.
-
Code Quality: The SDK includes development tools (black, ruff, mypy, pytest) to maintain high code quality standards.
Deploy to FTL:
# Deploy with auto-generated name
ftl deployContributions are welcome! Please feel free to submit a Pull Request.
# Clone the repository
git clone https://github.com/fastertools/ftl.git
cd ftl/sdk/python
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install development dependencies
make install-dev
# or manually:
pip install -e ".[dev]"
pip install componentize-py tox# Run all tests
make test
# Run tests with coverage
make test-cov
# Run tests for all Python versions
tox
# Run tests for specific Python version
tox -e py311
# Run specific test
pytest tests/test_ftl_sdk.py::test_tool_response_text# Format code
make format
# Run linting
make lint
# Type checking
make type-check
# Run all quality checks
make quality
# Or run individual tools:
black src tests
ruff check src tests
mypy srcmake help # Show all available commands
make install # Install SDK
make install-dev # Install with dev dependencies
make format # Format code with black
make lint # Run linting with ruff
make type-check # Run type checking with mypy
make test # Run tests
make test-cov # Run tests with coverage
make clean # Clean build artifacts
make publish # Publish to PyPISee CHANGELOG.md for a list of changes in each release.
Apache-2.0 - see LICENSE for details.