Skip to content

Epistates/TurboMCP

v1.0.9 Breaking

This release includes 2 breaking changes for platform teams planning a safe upgrade.

Published 8mo MCP Developer Tools
✓ No known CVEs patched
Read the diff → Tool health → What is this tool? →

✓ No known CVEs patched in this version

Topics

mcp mcp-client mcp-sdk mcp-server mcp-servers rust

Affected surfaces

auth rbac

Summary

AI summary

Prompt 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:

  1. list_prompts() was broken: Returned only prompt names (Vec<String>), losing critical metadata needed for UI generation
  2. get_prompt() ignored arguments: Did not support parameter substitution, breaking templated prompts
  3. 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

  1. list_prompts() now returns full Prompt objects with:

    • name: Programmatic identifier
    • title: Human-readable display name
    • description: Functional description
    • arguments: Complete schema for validation and UI generation
  2. get_prompt() now supports argument substitution:

    • Template prompts work with {parameter} syntax
    • Full MCP compliance for parameter passing
    • Backward compatible for prompts without arguments
  3. 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:

  1. Exposed synchronization primitives: Arc<Mutex<Client<T>>> leaked implementation details
  2. Non-cloneable types: Core types couldn't be shared easily across async tasks
  3. Poor ergonomics: Users had to manage Arc/Mutex themselves across the entire library
  4. Inconsistent patterns: Different components had different sharing approaches

Root Cause Analysis

  • Most TurboMCP methods require &mut self for 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

Track Epistates/TurboMCP

Get notified when new releases ship.

Sign up free

About Epistates/TurboMCP

TurboMCP SDK: Enterprise MCP SDK in Rust

All releases →

Beta — feedback welcome: [email protected]