Crates.io | ftl-sdk |
lib.rs | ftl-sdk |
version | 0.12.0 |
created_at | 2025-06-28 08:08:13.661748+00 |
updated_at | 2025-08-24 02:21:44.807369+00 |
description | Thin SDK providing MCP protocol types for FTL tool development |
homepage | |
repository | https://github.com/fastertools/ftl |
max_upload_size | |
id | 1729628 |
size | 52,059 |
Rust SDK for building Model Context Protocol (MCP) tools on WebAssembly.
[dependencies]
ftl-sdk = { version = "0.2.10", features = ["macros"] }
schemars = "0.8" # For automatic schema generation
serde = { version = "1.0", features = ["derive"] }
This SDK provides:
tools!
macro for defining multiple tools with minimal boilerplatetext!
, error!
, structured!
) for ergonomic responsestools!
MacroThe simplest way to create tools:
use ftl_sdk::{tools, text, ToolResponse};
use serde::Deserialize;
use schemars::JsonSchema;
#[derive(Deserialize, JsonSchema)]
struct AddInput {
/// First number to add
a: i32,
/// Second number to add
b: i32,
}
#[derive(Deserialize, JsonSchema)]
struct SubtractInput {
/// Number to subtract from
a: i32,
/// Number to subtract
b: i32,
}
tools! {
/// Adds two numbers together
fn add(input: AddInput) -> ToolResponse {
let result = input.a + input.b;
text!("{} + {} = {}", input.a, input.b, result)
}
/// Subtracts two numbers
fn subtract(input: SubtractInput) -> ToolResponse {
let result = input.a - input.b;
text!("{} - {} = {}", input.a, input.b, result)
}
}
The tools!
macro automatically:
For more control, implement the protocol manually:
use ftl_sdk::{ToolMetadata, ToolResponse};
use serde_json::json;
use spin_sdk::http::{IntoResponse, Method, Request, Response};
use spin_sdk::http_component;
#[http_component]
fn handle_tool(req: Request) -> anyhow::Result<impl IntoResponse> {
match *req.method() {
Method::Get => {
// Return array of tool metadata for multiple tools
let metadata = vec![
ToolMetadata {
name: "echo".to_string(),
description: Some("Echo tool".to_string()),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"message": { "type": "string" }
},
"required": ["message"]
}),
output_schema: None,
annotations: None,
meta: None,
},
ToolMetadata {
name: "reverse".to_string(),
description: Some("Reverse text".to_string()),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"text": { "type": "string" }
},
"required": ["text"]
}),
output_schema: None,
annotations: None,
meta: None,
},
];
Ok(Response::builder()
.status(200)
.header("Content-Type", "application/json")
.body(serde_json::to_vec(&metadata)?)
.build())
}
Method::Post => {
// Route based on path (e.g., /echo or /reverse)
let path = req.path();
let body_bytes = req.body();
let input: serde_json::Value = serde_json::from_slice(body_bytes)?;
let response = match path {
"/echo" => {
let message = input["message"].as_str().unwrap_or("");
ToolResponse::text(format!("Echo: {}", message))
}
"/reverse" => {
let text = input["text"].as_str().unwrap_or("");
let reversed: String = text.chars().rev().collect();
ToolResponse::text(reversed)
}
_ => ToolResponse::error(format!("Unknown tool: {}", path))
};
Ok(Response::builder()
.status(200)
.header("Content-Type", "application/json")
.body(serde_json::to_vec(&response)?)
.build())
}
_ => Ok(Response::builder()
.status(405)
.header("Allow", "GET, POST")
.body("Method not allowed")
.build())
}
}
Tools must be compiled to WebAssembly for the Spin platform:
# Cargo.toml
[dependencies]
ftl-sdk = { version = "0.2.10", features = ["macros"] }
schemars = "0.8"
serde = { version = "1.0", features = ["derive"] }
spin-sdk = "4.0"
[lib]
crate-type = ["cdylib"]
Build command:
cargo build --target wasm32-wasip1 --release
use ftl_sdk::{text, error, structured, ToolResponse, ToolContent};
use serde_json::json;
// Simple text response with macros
let response = text!("Hello, world!");
// With formatting
let response = text!("Hello, {}!", name);
// Error response
let response = error!("Something went wrong: {}", reason);
// Response with structured content
let data = serde_json::json!({ "result": 42 });
let response = structured!(data, "Calculation complete");
// Or use the builder methods directly
let response = ToolResponse::text("Hello, world!");
let response = ToolResponse::error("Something went wrong");
let response = ToolResponse::with_structured(
"Calculation complete",
serde_json::json!({ "result": 42 })
);
// Multiple content items
let response = ToolResponse {
content: vec![
ToolContent::text("Processing complete"),
ToolContent::image(base64_data, "image/png"),
],
structured_content: None,
is_error: None,
};
The tools!
macro supports async functions:
use ftl_sdk::{tools, text, ToolResponse};
use serde::Deserialize;
use schemars::JsonSchema;
#[derive(Deserialize, JsonSchema)]
struct WeatherInput {
location: String,
}
#[derive(Deserialize, JsonSchema)]
struct StatusInput {
service: String,
}
tools! {
/// Fetch weather data
async fn fetch_weather(input: WeatherInput) -> ToolResponse {
let weather = fetch_from_api(&input.location).await;
text!("Weather in {}: {}", input.location, weather)
}
/// Another async tool
async fn check_status(input: StatusInput) -> ToolResponse {
let status = get_status(&input.service).await;
text!("Status: {}", status)
}
}
Define as many tools as needed in one component:
use ftl_sdk::{tools, text, structured, ToolResponse};
use serde::Deserialize;
use schemars::JsonSchema;
use serde_json::json;
#[derive(Deserialize, JsonSchema)]
struct TextInput {
text: String,
}
#[derive(Deserialize, JsonSchema)]
struct DataInput {
data: serde_json::Value,
}
#[derive(Deserialize, JsonSchema)]
struct ReportInput {
topic: String,
}
tools! {
/// Process text
fn process_text(input: TextInput) -> ToolResponse {
text!("Processed: {}", input.text)
}
/// Analyze data
fn analyze_data(input: DataInput) -> ToolResponse {
let result = analyze(&input.data);
structured!(result, "Analysis complete")
}
/// Generate report
async fn generate_report(input: ReportInput) -> ToolResponse {
let report = create_report(&input).await;
text!("{}", report)
}
}
cargo build --target wasm32-wasip1 --release
cargo test
# Format code
cargo fmt
# Run linting
cargo clippy
# Run all checks
make quality
Apache-2.0 - see LICENSE for details.