|
| 1 | +//! Hybrid Workflow Execution Example |
| 2 | +//! |
| 3 | +//! This example demonstrates the hybrid execution model where the server executes |
| 4 | +//! deterministic steps and provides structured guidance for steps requiring LLM reasoning. |
| 5 | +//! |
| 6 | +//! # Use Case: Logseq Task Management with Fuzzy Matching |
| 7 | +//! |
| 8 | +//! The workflow adds a task to a Logseq project, but the user's input may not |
| 9 | +//! match the exact project name. The server: |
| 10 | +//! 1. Lists all available pages (deterministic API call) |
| 11 | +//! 2. Fetches and embeds task formatting documentation (resource) |
| 12 | +//! 3. Provides guidance for the client LLM to fuzzy-match the project name |
| 13 | +//! 4. Client LLM continues by matching "MCP Tester" to "mcp-tester" and formatting the task |
| 14 | +//! |
| 15 | +//! ## Key Concepts |
| 16 | +//! |
| 17 | +//! - **Server-side execution**: Deterministic operations (data fetching) |
| 18 | +//! - **Client-side reasoning**: Fuzzy matching, context-aware decisions |
| 19 | +//! - **Graceful handoff**: Server stops when it can't proceed, returns partial trace |
| 20 | +//! - **Guidance messages**: Help LLM understand what to do next |
| 21 | +//! - **Argument substitution**: `{arg_name}` in guidance → actual values |
| 22 | +
|
| 23 | +use async_trait::async_trait; |
| 24 | +use pmcp::server::workflow::{SequentialWorkflow, ToolHandle, WorkflowStep}; |
| 25 | +use pmcp::{ |
| 26 | + Content, ListResourcesResult, ReadResourceResult, ResourceHandler, Result, Server, SimpleTool, |
| 27 | +}; |
| 28 | +use serde_json::json; |
| 29 | + |
| 30 | +#[tokio::main] |
| 31 | +async fn main() -> Result<()> { |
| 32 | + println!("=== Hybrid Workflow Execution: Logseq Task Management ===\n"); |
| 33 | + |
| 34 | + // Define workflow with hybrid execution |
| 35 | + let workflow = create_logseq_task_workflow(); |
| 36 | + |
| 37 | + // Create mock Logseq tools |
| 38 | + let list_pages_tool = SimpleTool::new("list_pages", |_args, _extra| { |
| 39 | + Box::pin(async move { |
| 40 | + Ok(json!({ |
| 41 | + "page_names": [ |
| 42 | + "mcp-tester", |
| 43 | + "MCP Rust SDK", |
| 44 | + "Test Page", |
| 45 | + "Documentation", |
| 46 | + "rust-projects" |
| 47 | + ] |
| 48 | + })) |
| 49 | + }) |
| 50 | + }) |
| 51 | + .with_description("List all pages in Logseq knowledge base") |
| 52 | + .with_schema(json!({ |
| 53 | + "type": "object", |
| 54 | + "properties": {}, |
| 55 | + "required": [] |
| 56 | + })); |
| 57 | + |
| 58 | + let add_task_tool = SimpleTool::new("add_journal_task", |args, _extra| { |
| 59 | + Box::pin(async move { |
| 60 | + let task = args |
| 61 | + .get("formatted_task") |
| 62 | + .and_then(|v| v.as_str()) |
| 63 | + .unwrap_or(""); |
| 64 | + Ok(json!({ |
| 65 | + "success": true, |
| 66 | + "task_id": "task-123", |
| 67 | + "task": task, |
| 68 | + "created_at": "2025-10-03T10:30:00Z" |
| 69 | + })) |
| 70 | + }) |
| 71 | + }) |
| 72 | + .with_description("Add a task to Logseq journal") |
| 73 | + .with_schema(json!({ |
| 74 | + "type": "object", |
| 75 | + "properties": { |
| 76 | + "formatted_task": {"type": "string"} |
| 77 | + }, |
| 78 | + "required": ["formatted_task"] |
| 79 | + })); |
| 80 | + |
| 81 | + // Create mock resource handler for task formatting documentation |
| 82 | + struct LogseqDocsHandler; |
| 83 | + |
| 84 | + #[async_trait] |
| 85 | + impl ResourceHandler for LogseqDocsHandler { |
| 86 | + async fn read( |
| 87 | + &self, |
| 88 | + uri: &str, |
| 89 | + _extra: pmcp::RequestHandlerExtra, |
| 90 | + ) -> Result<ReadResourceResult> { |
| 91 | + match uri { |
| 92 | + "docs://logseq/task-format" => Ok(ReadResourceResult { |
| 93 | + contents: vec![Content::Text { |
| 94 | + text: r#"Logseq Task Formatting Guide |
| 95 | +================================ |
| 96 | +
|
| 97 | +Task Format: |
| 98 | +- Use [[page-name]] to link to pages |
| 99 | +- Add TODO prefix for tasks |
| 100 | +- Format: TODO [[page-name]] Task description |
| 101 | +
|
| 102 | +Examples: |
| 103 | +- TODO [[mcp-tester]] Fix workflow execution bug |
| 104 | +- TODO [[rust-projects]] Update documentation |
| 105 | +- TODO [[Documentation]] Add examples |
| 106 | +
|
| 107 | +Best Practices: |
| 108 | +- Always use lowercase, hyphenated page names |
| 109 | +- Be specific in task descriptions |
| 110 | +- Link to the relevant project page |
| 111 | +"# |
| 112 | + .to_string(), |
| 113 | + }], |
| 114 | + }), |
| 115 | + _ => Err(pmcp::Error::validation(format!( |
| 116 | + "Unknown resource: {}", |
| 117 | + uri |
| 118 | + ))), |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + async fn list( |
| 123 | + &self, |
| 124 | + _cursor: Option<String>, |
| 125 | + _extra: pmcp::RequestHandlerExtra, |
| 126 | + ) -> Result<ListResourcesResult> { |
| 127 | + Ok(ListResourcesResult { |
| 128 | + resources: vec![pmcp::ResourceInfo { |
| 129 | + uri: "docs://logseq/task-format".to_string(), |
| 130 | + name: "Logseq Task Formatting Guide".to_string(), |
| 131 | + description: Some("Guide for formatting tasks in Logseq".to_string()), |
| 132 | + mime_type: Some("text/plain".to_string()), |
| 133 | + }], |
| 134 | + next_cursor: None, |
| 135 | + }) |
| 136 | + } |
| 137 | + } |
| 138 | + |
| 139 | + // Build server with workflow |
| 140 | + let server = Server::builder() |
| 141 | + .name("logseq-task-server") |
| 142 | + .version("1.0.0") |
| 143 | + .tool("list_pages", list_pages_tool) |
| 144 | + .tool("add_journal_task", add_task_tool) |
| 145 | + .resources(LogseqDocsHandler) |
| 146 | + .prompt_workflow(workflow)? |
| 147 | + .build()?; |
| 148 | + |
| 149 | + println!("✓ Server built successfully\n"); |
| 150 | + |
| 151 | + // ================================================================= |
| 152 | + // Demonstrate Hybrid Execution |
| 153 | + // ================================================================= |
| 154 | + |
| 155 | + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); |
| 156 | + println!("📝 User Invokes Workflow"); |
| 157 | + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); |
| 158 | + |
| 159 | + println!("User types:"); |
| 160 | + println!(" /add_project_task \"MCP Tester\" \"Fix workflow execution bug\"\n"); |
| 161 | + |
| 162 | + println!("Note: User typed \"MCP Tester\" (with capitals and space)"); |
| 163 | + println!(" But the actual page name is \"mcp-tester\" (lowercase, hyphenated)\n"); |
| 164 | + |
| 165 | + // Get the workflow prompt handler |
| 166 | + let prompt_handler = server |
| 167 | + .get_prompt("add_project_task") |
| 168 | + .expect("Workflow should be registered"); |
| 169 | + |
| 170 | + let mut args = std::collections::HashMap::new(); |
| 171 | + args.insert("project".to_string(), "MCP Tester".to_string()); |
| 172 | + args.insert("task".to_string(), "Fix workflow execution bug".to_string()); |
| 173 | + |
| 174 | + let extra = pmcp::server::cancellation::RequestHandlerExtra { |
| 175 | + cancellation_token: Default::default(), |
| 176 | + request_id: "demo-request".to_string(), |
| 177 | + session_id: None, |
| 178 | + auth_info: None, |
| 179 | + auth_context: None, |
| 180 | + }; |
| 181 | + |
| 182 | + // Execute workflow (hybrid execution) |
| 183 | + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); |
| 184 | + println!("⚙️ Server Executing Workflow (Hybrid Mode)"); |
| 185 | + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); |
| 186 | + |
| 187 | + let result = prompt_handler |
| 188 | + .handle(args, extra) |
| 189 | + .await |
| 190 | + .expect("Workflow execution should succeed"); |
| 191 | + |
| 192 | + println!( |
| 193 | + "Server executed what it could and returned {} messages\n", |
| 194 | + result.messages.len() |
| 195 | + ); |
| 196 | + |
| 197 | + // ================================================================= |
| 198 | + // Display Conversation Trace |
| 199 | + // ================================================================= |
| 200 | + |
| 201 | + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); |
| 202 | + println!("💬 Conversation Trace (Returned to Client LLM)"); |
| 203 | + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); |
| 204 | + |
| 205 | + for (i, msg) in result.messages.iter().enumerate() { |
| 206 | + println!("Message {} [{:?}]:", i + 1, msg.role); |
| 207 | + if let pmcp::types::MessageContent::Text { text } = &msg.content { |
| 208 | + println!("{}\n", text); |
| 209 | + } |
| 210 | + } |
| 211 | + |
| 212 | + // ================================================================= |
| 213 | + // Explain What Happens Next |
| 214 | + // ================================================================= |
| 215 | + |
| 216 | + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); |
| 217 | + println!("🤖 Client LLM Continues Execution"); |
| 218 | + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); |
| 219 | + |
| 220 | + println!("The client LLM receives the conversation trace above and:"); |
| 221 | + println!("1. Sees the list of available pages"); |
| 222 | + println!("2. Reads the guidance on what to do next"); |
| 223 | + println!("3. Matches \"MCP Tester\" to \"mcp-tester\" (fuzzy matching)"); |
| 224 | + println!("4. Formats the task as: [[mcp-tester]] Fix workflow execution bug"); |
| 225 | + println!("5. Calls add_journal_task with the formatted task\n"); |
| 226 | + |
| 227 | + println!("Expected client response:"); |
| 228 | + println!("┌────────────────────────────────────────────────┐"); |
| 229 | + println!("│ I can see \"mcp-tester\" in the page list, │"); |
| 230 | + println!("│ which matches \"MCP Tester\" (case-insensitive, │"); |
| 231 | + println!("│ with hyphen instead of space). │"); |
| 232 | + println!("│ │"); |
| 233 | + println!("│ I'll format the task with the correct link: │"); |
| 234 | + println!("│ │"); |
| 235 | + println!("│ <function_calls> │"); |
| 236 | + println!("│ <invoke name=\"add_journal_task\"> │"); |
| 237 | + println!("│ <parameter name=\"formatted_task\"> │"); |
| 238 | + println!("│ [[mcp-tester]] Fix workflow execution bug│"); |
| 239 | + println!("│ </parameter> │"); |
| 240 | + println!("│ </invoke> │"); |
| 241 | + println!("│ </function_calls> │"); |
| 242 | + println!("└────────────────────────────────────────────────┘\n"); |
| 243 | + |
| 244 | + // ================================================================= |
| 245 | + // Key Takeaways |
| 246 | + // ================================================================= |
| 247 | + |
| 248 | + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); |
| 249 | + println!("🎯 Key Takeaways: Hybrid Execution Model"); |
| 250 | + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); |
| 251 | + |
| 252 | + println!("✓ Server handles deterministic operations:"); |
| 253 | + println!(" - Listing pages (API call)"); |
| 254 | + println!(" - Fetching and embedding resource content"); |
| 255 | + println!(" - Data fetching, validation"); |
| 256 | + println!(" - Structured data operations\n"); |
| 257 | + |
| 258 | + println!("✓ Client LLM handles reasoning:"); |
| 259 | + println!(" - Fuzzy matching (\"MCP Tester\" → \"mcp-tester\")"); |
| 260 | + println!(" - Context-aware decisions"); |
| 261 | + println!(" - Natural language understanding\n"); |
| 262 | + |
| 263 | + println!("✓ Guidance enables seamless handoff:"); |
| 264 | + println!(" - with_guidance() provides instructions to LLM"); |
| 265 | + println!(" - {{arg_name}} syntax substitutes actual values"); |
| 266 | + println!(" - Clear separation of concerns\n"); |
| 267 | + |
| 268 | + println!("✓ Benefits:"); |
| 269 | + println!(" - Efficient: Server does what it can"); |
| 270 | + println!(" - Intelligent: LLM handles complex reasoning"); |
| 271 | + println!(" - Maintainable: Declarative workflow definition"); |
| 272 | + println!(" - Flexible: Works for simple and complex cases\n"); |
| 273 | + |
| 274 | + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); |
| 275 | + println!("📚 When to Use Hybrid Execution"); |
| 276 | + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); |
| 277 | + |
| 278 | + println!("Use hybrid execution when:"); |
| 279 | + println!(" ❌ Server can't match user input to structured data"); |
| 280 | + println!(" ❌ Context-aware decisions are needed"); |
| 281 | + println!(" ❌ Multiple valid options require LLM judgment"); |
| 282 | + println!(" ❌ User might need to clarify intent\n"); |
| 283 | + |
| 284 | + println!("Use full server-side execution when:"); |
| 285 | + println!(" ✅ All parameters are deterministic"); |
| 286 | + println!(" ✅ No fuzzy matching or reasoning needed"); |
| 287 | + println!(" ✅ Single-shot execution is sufficient\n"); |
| 288 | + |
| 289 | + Ok(()) |
| 290 | +} |
| 291 | + |
| 292 | +fn create_logseq_task_workflow() -> SequentialWorkflow { |
| 293 | + SequentialWorkflow::new( |
| 294 | + "add_project_task", |
| 295 | + "add a task to a Logseq project with intelligent project name matching", |
| 296 | + ) |
| 297 | + .argument("project", "The project name (can be fuzzy, e.g., 'MCP Tester')", true) |
| 298 | + .argument("task", "The task description", true) |
| 299 | + // Step 1: Server executes this (deterministic) |
| 300 | + .step( |
| 301 | + WorkflowStep::new("list_pages", ToolHandle::new("list_pages")) |
| 302 | + .with_guidance("I'll first get all available page names from Logseq") |
| 303 | + .bind("pages"), |
| 304 | + ) |
| 305 | + // Step 2: Client continues (needs LLM reasoning for fuzzy matching) |
| 306 | + // This step has guidance but no .arg() mappings |
| 307 | + // The tool schema requires 'formatted_task' parameter (see tool definition) |
| 308 | + // Server can't provide it -> automatic handoff to client |
| 309 | + // Guidance tells the client LLM what to do |
| 310 | + // Resource is fetched and embedded by server before handoff |
| 311 | + .step( |
| 312 | + WorkflowStep::new("add_task", ToolHandle::new("add_journal_task")) |
| 313 | + .with_guidance( |
| 314 | + "I'll now:\n\ |
| 315 | + 1. Find the page name from the list above that best matches '{project}'\n\ |
| 316 | + 2. Format the task according to the guide below\n\ |
| 317 | + 3. Call add_journal_task with the formatted_task parameter", |
| 318 | + ) |
| 319 | + .with_resource("docs://logseq/task-format") |
| 320 | + .expect("Valid resource URI") |
| 321 | + // No .arg() mappings - server will detect this doesn't satisfy schema |
| 322 | + // and gracefully hand off to client LLM |
| 323 | + .bind("result"), |
| 324 | + ) |
| 325 | +} |
0 commit comments