This release includes 2 breaking changes for platform teams planning a safe upgrade.
✓ No known CVEs patched in this version
Topics
Affected surfaces
Summary
AI summaryPrompt API now returns full metadata and supports argument substitution per MCP spec.
Full changelog
🚨 Critical Fix: MCP Protocol Compliance for Prompts
Issue Identified
User feedback from an internal integration revealed that TurboMCP's prompt API was non-compliant with the MCP 2025-06-18 specification:
list_prompts()was broken: Returned only prompt names (Vec<String>), losing critical metadata needed for UI generationget_prompt()ignored arguments: Did not support parameter substitution, breaking templated prompts- Schema information inaccessible: Frontend applications couldn't generate dynamic forms
Root Cause
The simplified API methods bypassed the full protocol implementation in execute_with_plugins(), resulting in:
- Protocol violations
- Broken user experience for prompt templating
🎯 Solution: Protocol-Compliant API Redesign
API Changes
Before (v1.0.8 - Broken)
// ❌ NON-COMPLIANT: Lost metadata
pub async fn list_prompts(&mut self) -> Result<Vec<String>>
// ❌ BROKEN: No argument support
pub async fn get_prompt(&mut self, name: &str) -> Result<GetPromptResult>
After (v1.0.9 - MCP Compliant)
// ✅ SPEC-COMPLIANT: Full Prompt objects per MCP spec
pub async fn list_prompts(&mut self) -> Result<Vec<Prompt>>
// ✅ FULL SUPPORT: Argument substitution per MCP spec
pub async fn get_prompt(&mut self, name: &str, arguments: Option<PromptInput>) -> Result<GetPromptResult>
What's Fixed
-
list_prompts()now returns fullPromptobjects with:name: Programmatic identifiertitle: Human-readable display namedescription: Functional descriptionarguments: Complete schema for validation and UI generation
-
get_prompt()now supports argument substitution:- Template prompts work with
{parameter}syntax - Full MCP compliance for parameter passing
- Backward compatible for prompts without arguments
- Template prompts work with
-
Schema information directly accessible:
- No separate methods needed
- Schemas available from
list_prompts()results - Perfect for dynamic UI form generation
🔄 Major Enhancement: Comprehensive Shared Wrappers for Async Concurrency
Issue Addressed
Feedback from Rig integration PR revealed that TurboMCP's client async usage patterns exposed Arc/Mutex complexity in public APIs:
- Exposed synchronization primitives:
Arc<Mutex<Client<T>>>leaked implementation details - Non-cloneable types: Core types couldn't be shared easily across async tasks
- Poor ergonomics: Users had to manage Arc/Mutex themselves across the entire library
- Inconsistent patterns: Different components had different sharing approaches
Root Cause Analysis
- Most TurboMCP methods require
&mut selffor state management - Types implement Send + Sync but cannot be mutated concurrently
- MCP protocol requires stateful connections with message correlation
- Users needed thread-safe sharing for concurrent operations across multiple components
Solution: Comprehensive Shared Wrapper System
We implemented a complete shared wrapper system that provides clean, ergonomic APIs for all major TurboMCP components:
1. SharedClient<T: Transport> - Thread-Safe Client Access
use turbomcp_client::{Client, SharedClient};
// Create a thread-safe, cloneable client wrapper
let transport = StdioTransport::new();
let client = Client::new(transport);
let shared = SharedClient::new(client);
// Initialize once
shared.initialize().await?;
// Clone for concurrent usage
let shared1 = shared.clone();
let shared2 = shared.clone();
// Concurrent operations work seamlessly
let handle1 = tokio::spawn(async move {
shared1.list_tools().await
});
let handle2 = tokio::spawn(async move {
shared2.list_prompts().await
});
let (tools, prompts) = tokio::join!(handle1, handle2);
2. SharedTransport<T: Transport> - Thread-Safe Transport Layer
use turbomcp_transport::{StdioTransport, SharedTransport};
// Wrap any transport for concurrent access
let transport = StdioTransport::new();
let shared = SharedTransport::new(transport);
// Connect once
shared.connect().await?;
// Clone for sharing across tasks
let shared1 = shared.clone();
let shared2 = shared.clone();
// Both tasks can use the transport concurrently
let handle1 = tokio::spawn(async move {
shared1.send(message).await
});
let handle2 = tokio::spawn(async move {
shared2.receive().await
});
3. SharedServer - Thread-Safe Server Management
use turbomcp_server::{McpServer, SharedServer};
// Wrap server for monitoring while running
let server = McpServer::new(config);
let shared = SharedServer::new(server);
// Clone for monitoring tasks
let monitor = shared.clone();
let metrics_task = tokio::spawn(async move {
loop {
let health = monitor.health().await;
println!("Server health: {:?}", health);
tokio::time::sleep(Duration::from_secs(5)).await;
}
});
// Run the server (consumes the shared wrapper)
shared.run_stdio().await?;
4. Generic Shareable Pattern - Reusable Abstraction
use turbomcp_core::shared::{Shared, ConsumableShared, Shareable};
// Generic shared wrapper for any type
let counter = MyCounter::new();
let shared = Shared::new(counter);
// Use with closures for fine-grained control
shared.with_mut(|c| c.increment()).await;
let value = shared.with(|c| c.get()).await;
// Consumable variant for one-time use types
let server = MyServer::new();
let shared = ConsumableShared::new(server);
// Access until consumption
shared.with(|s| s.status()).await?;
let server = shared.consume().await?; // Extracts the server
5. SharedElicitationCoordinator - Thread-Safe Elicitation
use turbomcp_server::elicitation::SharedElicitationCoordinator;
// Clone for concurrent elicitation handling
let coordinator = SharedElicitationCoordinator::default();
let coord1 = coordinator.clone();
let coord2 = coordinator.clone();
// Both tasks can coordinate elicitation concurrently
let handle1 = tokio::spawn(async move {
coord1.coordinate_request(request1).await
});
let handle2 = tokio::spawn(async move {
coord2.coordinate_request(request2).await
});
Key Design Principles
- Encapsulated Arc/Mutex: Implementation details hidden from users
- Clone Support: Easy sharing across async tasks
- Identical API: All original methods available with same signatures
- Zero Overhead: Same performance as direct usage
- MCP Compliant: Preserves all protocol behavior exactly
- Pattern Consistency: Same sharing approach across all components
For Library Integration (Rig)
// Before (problematic)
pub struct TurboMcpTool {
client: Arc<Mutex<Client<T>>>, // ❌ Exposed complexity
transport: Arc<Mutex<Transport>>, // ❌ More exposed complexity
}
// After (clean)
pub struct TurboMcpTool<T: Transport> {
client: SharedClient<T>, // ✅ Clean, cloneable
transport: SharedTransport<T>, // ✅ Consistent pattern
}
// Even better (generic)
pub struct TurboMcpTool<C: Clone + Send + Sync> {
client: C, // ✅ Works with any shared client type
}
Benefits of Comprehensive Shared Wrapper System
- Clean Public APIs: No exposed Arc/Mutex types throughout library
- Better Ergonomics: Clone-able types for easy async sharing
- Thread Safety: Concurrent access without data races across all components
- Protocol Compliance: Maintains strict MCP semantics everywhere
- Backward Compatible: Existing code unchanged, opt-in adoption
- Pattern Consistency: Same sharing approach from client to server to transport
- Comprehensive Testing: 25+ additional test cases for thread safety across all wrappers
📝 Usage Examples
Getting Prompts with Full Metadata
use turbomcp_client::Client;
let mut client = Client::new(transport);
client.initialize().await?;
// Get all prompts with complete schema information
let prompts = client.list_prompts().await?;
for prompt in prompts {
println!("Prompt: {} - {}", prompt.name, prompt.title.unwrap_or_default());
// Access schema directly for UI generation
if let Some(args) = prompt.arguments {
for arg in args {
let required = arg.required.unwrap_or(false);
println!(" - {}: {} (required: {})",
arg.name,
arg.description.unwrap_or_default(),
required
);
}
}
}
Executing Prompts with Arguments
use std::collections::HashMap;
// Get template form (no substitution)
let template = client.get_prompt("greeting", None).await?;
// Execute with parameters
let mut args = HashMap::new();
args.insert("name".to_string(), serde_json::Value::String("Alice".to_string()));
args.insert("greeting".to_string(), serde_json::Value::String("Hello".to_string()));
let result = client.get_prompt("greeting", Some(args)).await?;
Shared Wrappers Integration Workflow
use turbomcp::{
client::{Client, SharedClient},
transport::{StdioTransport, SharedTransport},
server::{McpServer, SharedServer},
core::shared::{Shared, ConsumableShared},
};
// Complete integration example
async fn integrated_mcp_workflow() -> Result<(), Box<dyn std::error::Error>> {
// 1. Create shared transport
let transport = StdioTransport::new();
let shared_transport = SharedTransport::new(transport);
shared_transport.connect().await?;
// 2. Create shared client
let client = Client::new(shared_transport.clone());
let shared_client = SharedClient::new(client);
shared_client.initialize().await?;
// 3. Create shared server
let server = McpServer::new(Default::default());
let shared_server = SharedServer::new(server);
// 4. Clone for concurrent usage
let client_clone = shared_client.clone();
let server_monitor = shared_server.clone();
let transport_monitor = shared_transport.clone();
// 5. Concurrent operations
let client_task = tokio::spawn(async move {
let tools = client_clone.list_tools().await?;
let prompts = client_clone.list_prompts().await?;
Ok::<_, Box<dyn std::error::Error>>((tools, prompts))
});
let monitor_task = tokio::spawn(async move {
loop {
let server_health = server_monitor.health().await;
let transport_connected = transport_monitor.is_connected().await;
println!("Server: {:?}, Transport: {}", server_health, transport_connected);
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
}
});
// 6. Run server (consumes it)
let server_task = tokio::spawn(async move {
shared_server.run_stdio().await
});
// All tasks run concurrently with proper shared access
let (client_result, _, server_result) = tokio::try_join!(
client_task,
tokio::time::timeout(std::time::Duration::from_secs(10), monitor_task),
server_task
)?;
Ok(())
}
🧪 Testing
Comprehensive Test Suite
- 35+ new test cases covering all functionality
- Protocol compliance verification across all shared wrappers
- Error handling validation for concurrent scenarios
- Argument schema accessibility
- Thread safety validation for all shared wrappers
- Performance benchmarking for concurrent access patterns
Test Coverage
Prompt API Testing
- ✅ Full schema information returned by
list_prompts() - ✅ Argument substitution in
get_prompt() - ✅ Template forms without arguments
- ✅ Error handling for edge cases
- ✅ Integration workflow simulation
Shared Wrappers Testing
- ✅ SharedClient: 9 test cases for concurrent client access
- ✅ SharedTransport: 6 test cases for transport sharing across tasks
- ✅ SharedServer: 8 test cases for server lifecycle management
- ✅ SharedElicitationCoordinator: 4 test cases for concurrent elicitation
- ✅ Generic Shareable Pattern: 8 test cases for Shared and ConsumableShared
- ✅ Thread Safety: All wrappers tested with tokio::spawn concurrent access
- ✅ Clone Behavior: Verified Arc sharing semantics across all wrappers
- ✅ API Parity: All wrapper methods tested against original implementations
🔄 Migration Guide
Breaking Changes
This release contains breaking changes to the prompt API. However, the previous implementation was non-compliant and broken, so migration is necessary for proper functionality.
Updating list_prompts() calls
// OLD (v1.0.8) - Broken
let prompt_names: Vec<String> = client.list_prompts().await?;
for name in prompt_names {
println!("Prompt: {}", name);
}
// NEW (v1.0.9) - Fixed
let prompts: Vec<Prompt> = client.list_prompts().await?;
for prompt in prompts {
println!("Prompt: {} ({})", prompt.name, prompt.title.unwrap_or_default());
}
Updating get_prompt() calls
// OLD (v1.0.8) - No argument support
let result = client.get_prompt("greeting").await?;
// NEW (v1.0.9) - Full argument support
let result = client.get_prompt("greeting", None).await?; // Template form
let result = client.get_prompt("greeting", Some(args)).await?; // With substitution
Adopting Shared Wrappers (Optional - Opt-in)
// OLD (still supported) - Manual Arc/Mutex management
let client = Arc::new(Mutex::new(Client::new(transport)));
let client_clone = Arc::clone(&client);
let handle = tokio::spawn(async move {
let client = client_clone.lock().await;
// Limited API due to lock guard lifetime
});
// NEW (recommended) - Clean shared wrapper
let client = Client::new(transport);
let shared = SharedClient::new(client);
let shared_clone = shared.clone();
let handle = tokio::spawn(async move {
shared_clone.list_tools().await // Full API available
});
Benefits of Migration
- Fixes broken functionality: Template prompts now work
- Enables rich UIs: Full metadata for better user experience
- Future-proof: 100% MCP specification compliance
- Better developer experience: Cleaner, more intuitive API
- Improved async ergonomics: Clean APIs for concurrent access across all components
- Library integration ready: Perfect for embedding in frameworks like Rig
Breaking Changes
- `list_prompts()` now returns full `Prompt` objects with name, title, description, and arguments instead of just names.
- `get_prompt(&str)` removed; replaced with `get_prompt(&str, Option<Args>)` to support argument substitution.
Weekly OSS security release digest.
The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.
No spam, unsubscribe anytime.
Share this release
Related context
Beta — feedback welcome: [email protected]