plugins/turul-mcp-skills/skills/mcp-client-patterns/SKILL.md
This skill should be used when the user asks about "MCP client", "McpClient", "McpClientBuilder", "connect to MCP server", "HttpTransport", "SseTransport", "tool call from client", "client session", "client task workflow", "ToolCallResponse", "client error handling", "disconnect", "client configuration", "refresh_tools", "tool change notification", or "list_changed client". Covers transport selection, connection lifecycle, tool/resource/prompt invocation, task workflows, tool change notifications, and error handling for the Turul MCP Client (turul-mcp-client crate, Rust). Do NOT use for server-side work (tools, resources, prompts) — see tool-creation-patterns and resource-prompt-patterns.
npx skillsauth add aussierobots/turul-mcp-framework mcp-client-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Transport is auto-detected from the URL:
McpClientBuilder::new().with_url(url)?
├─ URL contains /sse or ?transport=sse ──→ SseTransport (Legacy HTTP+SSE, MCP 2024-11-05)
└─ Otherwise (default) ─────────────────→ HttpTransport (Streamable HTTP, MCP 2025-11-25)
Or build explicitly:
// Auto-detect (recommended)
McpClientBuilder::new().with_url("http://host/mcp")?
// Explicit HTTP (Streamable HTTP, MCP 2025-11-25)
McpClientBuilder::new().with_transport(Box::new(HttpTransport::new("http://host/mcp")?))
// Explicit SSE (Legacy HTTP+SSE, MCP 2024-11-05)
McpClientBuilder::new().with_transport(Box::new(SseTransport::new("http://host/sse")?))
| Feature | HttpTransport | SseTransport |
|---|---|---|
| Protocol | MCP 2025-11-25 (Streamable HTTP) | MCP 2024-11-05 (Legacy HTTP+SSE) |
| Server events | SSE streaming on response | Separate SSE endpoint |
| Session management | Mcp-Session-Id header | Mcp-Session-Id header |
| Recommended for | New servers | Legacy servers only |
See references/transport-guide.md for full details.
// turul-mcp-client v0.3
use turul_mcp_client::{McpClientBuilder, McpClientResult};
use serde_json::json;
#[tokio::main]
async fn main() -> McpClientResult<()> {
// Build client — transport auto-detected from URL
let client = McpClientBuilder::new()
.with_url("http://localhost:8080/mcp")?
.build();
// Connect (performs initialize handshake)
client.connect().await?;
// Use the client
let tools = client.list_tools().await?;
let result = client.call_tool("add", json!({"a": 1, "b": 2})).await?;
println!("{result:?}");
// Clean up (sends DELETE to server)
client.disconnect().await?;
Ok(())
}
For custom client identity, build a ClientConfig:
// turul-mcp-client v0.3
use turul_mcp_client::config::{ClientConfig, ClientInfo};
let config = ClientConfig {
client_info: ClientInfo {
name: "my-app".into(),
version: "1.0.0".into(),
..Default::default()
},
..Default::default()
};
let client = McpClientBuilder::new()
.with_url("http://localhost:8080/mcp")?
.with_config(config)
.build();
These are the most frequently used methods. For the full API surface, see the McpClient source in crates/turul-mcp-client/src/client.rs.
| Method | Parameters | Returns |
|---|---|---|
| connect() | &self | McpClientResult<()> |
| disconnect() | &self | McpClientResult<()> |
| is_ready() | &self | bool |
| list_tools() | &self | McpClientResult<Vec<Tool>> |
| list_tools_paginated(cursor) | Option<Cursor> | McpClientResult<ListToolsResult> |
| call_tool(name, args) | &str, Value | McpClientResult<CallToolResult> |
| call_tool_with_task(name, args, ttl) | &str, Value, Option<i64> | McpClientResult<ToolCallResponse> |
| list_resources() | &self | McpClientResult<Vec<Resource>> |
| list_resource_templates() | &self | McpClientResult<Vec<ResourceTemplate>> |
| read_resource(uri) | &str | McpClientResult<Vec<ResourceContent>> |
| list_prompts() | &self | McpClientResult<Vec<Prompt>> |
| get_prompt(name, args) | &str, Option<Value> | McpClientResult<GetPromptResult> |
| get_task(id) | &str | McpClientResult<Task> |
| get_task_result(id) | &str | McpClientResult<Value> (blocks until terminal) |
| cancel_task(id) | &str | McpClientResult<Task> |
| ping() | &self | McpClientResult<()> |
Additional methods: list_resources_paginated(), list_resource_templates_paginated(), list_prompts_paginated(), list_tasks(), list_tasks_paginated(), connection_status(), session_info(), transport_stats().
call_tool_with_task() returns a ToolCallResponse enum — either the result immediately or a task handle for long-running operations.
// turul-mcp-client v0.3
use turul_mcp_client::ToolCallResponse;
use turul_mcp_protocol::TaskStatus;
let response = client.call_tool_with_task("slow_add", json!({"a": 1, "b": 2}), None).await?;
match response {
ToolCallResponse::Immediate(result) => {
println!("Immediate result: {result:?}");
}
ToolCallResponse::TaskCreated(task) => {
// Option A: Block until terminal (per MCP spec)
let value = client.get_task_result(&task.task_id).await?;
// Option B: Poll for status
loop {
let task = client.get_task(&task.task_id).await?;
match task.status {
TaskStatus::Working => { /* continue polling */ }
TaskStatus::Completed => { break; }
TaskStatus::Failed => { eprintln!("Task failed"); break; }
TaskStatus::InputRequired => {
// Server needs client input (elicitation).
// McpClient does not yet expose an elicitation response API.
// Handle at application level or via raw JSON-RPC.
eprintln!("Task requires input — not yet supported by McpClient");
break;
}
TaskStatus::Cancelled => { break; }
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
}
}
Helper methods on ToolCallResponse:
response.is_task() — returns true if TaskCreatedresponse.task() — returns Option<&Task>response.immediate_result() — returns Option<&CallToolResult>Known gap: McpClient does not expose an elicitation response method. InputRequired status must be handled at the application level or via raw JSON-RPC.
See examples/task-workflow.rs for a complete example.
ClientConfig is a nested struct — all fields have sensible defaults.
// turul-mcp-client v0.3
use turul_mcp_client::config::*;
use std::time::Duration;
let config = ClientConfig {
client_info: ClientInfo {
name: "my-app".into(),
version: "2.0.0".into(),
description: Some("My MCP client app".into()),
vendor: Some("My Company".into()),
metadata: None,
},
timeouts: TimeoutConfig {
connect: Duration::from_secs(10), // default: 10s
request: Duration::from_secs(30), // default: 30s
long_operation: Duration::from_secs(300), // default: 300s
initialization: Duration::from_secs(15), // default: 15s
heartbeat: Duration::from_secs(30), // default: 30s
},
retry: RetryConfig {
max_attempts: 3, // default: 3
initial_delay: Duration::from_millis(100), // default: 100ms
max_delay: Duration::from_secs(10), // default: 10s
backoff_multiplier: 2.0, // default: 2.0
jitter: 0.1, // default: 0.1
exponential_backoff: true, // default: true
},
connection: ConnectionConfig::default(),
logging: LoggingConfig::default(),
};
Pass via .with_config(config) on McpClientBuilder.
McpClientError is a nested enum with sub-error types.
| Variant | Sub-errors | Retryable? |
|---|---|---|
| Transport(TransportError) | Http, Sse, Stdio, Unsupported, ConnectionFailed, Closed | ConnectionFailed + Closed yes |
| Protocol(ProtocolError) | InvalidRequest, InvalidResponse, UnsupportedVersion, MethodNotFound, InvalidParams, NegotiationFailed, CapabilityMismatch | No |
| Session(SessionError) | NotInitialized, AlreadyInitialized, Expired, Terminated, InvalidState, RecoveryFailed | No |
| Connection(reqwest::Error) | Network errors | Yes |
| Timeout | Operation timed out | Yes |
| ServerError { code, message, data } | JSON-RPC server error | Codes -32099..-32000 yes |
| Auth(String) | Authentication failures | No |
| Json(serde_json::Error) | Parse errors | No |
| Config(String) | Configuration errors | No |
| Generic { message } | Catch-all | No |
Built-in helpers:
error.is_retryable() — true for transport failures, connection errors, timeouts, retryable server codeserror.is_protocol_error() — true for protocol violationserror.is_session_error() — true for session lifecycle issueserror.error_code() — extracts JSON-RPC error code if availableUse RetryConfig::delay_for_attempt(n) to calculate backoff delay with jitter.
Session status codes (MCP 2025-11-25 Streamable HTTP):
| HTTP Status | Meaning | Client Action |
|---|---|---|
| 401 | Missing or invalid auth token, OR missing Mcp-Session-Id header | Re-authenticate or add session header |
| 404 | Session ID not found or terminated | Start fresh initialize handshake (do NOT re-authenticate) |
| 500 | Server internal error | Retry with backoff |
The 401 vs 404 distinction matters: 404 means "your session is gone, create a new one" — not an auth problem.
See references/error-handling-guide.md for full variant catalog and retry patterns.
When a server uses ToolChangeMode::Dynamic, clients receive notifications/tools/list_changed. The client caches tools and auto-invalidates on notification:
// turul-mcp-client v0.3
// list_tools() returns cached tools; refresh_tools() forces a fresh tools/list call
let tools = client.list_tools().await?; // Cached (fast)
let tools = client.refresh_tools().await?; // Forces fresh fetch
// After server emits notifications/tools/list_changed,
// the next list_tools() call automatically fetches fresh data.
For a complete dynamic tools E2E example, see examples/dynamic-tools-test-client.
client.connect().await? before operations — session not initialized, all calls fail with SessionError::NotInitializedclient.disconnect().await? — server session leaks (Drop spawns cleanup but is best-effort)SseTransport for MCP 2025-11-25 servers — wrong protocol; use HttpTransport or let with_url() auto-detectToolCallResponse::TaskCreated — call_tool_with_task() can return a task handle instead of immediate resulterror.is_retryable() — retrying non-retryable errors wastes time and hides bugsClientInfo — server logs show generic "mcp-client"; set name and version for debuggabilitytokio::time::sleep, not std::thread::sleepThis skill covers transport and lifecycle, not authentication. MCP clients that connect to OAuth-protected resource servers need to handle the authorization flow:
WWW-Authenticate: Bearer header containing a resource_metadata URL/.well-known/oauth-protected-resource from the RS to discover authorization_servers/.well-known/oauth-authorization-server from the AS to discover endpointsresource parameter at both /authorize and /tokenThe turul-mcp-client crate handles transport and session lifecycle. Token acquisition and header injection are the client application's responsibility.
See: the auth-patterns skill for RS-side validation and the authorization-server-patterns skill for building a demo AS to test against.
Per MCP authorization, the bearer token MUST be present on every HTTP request a client makes — that includes the POST request stream, the GET SSE listener, and the DELETE cleanup. A long-lived client that holds a token across rotations needs to swap the token in place rather than rebuild the whole client (which would drop the connection pool).
// turul-mcp-client v0.3
// Rotate the bearer on a long-lived client without losing the connection pool.
// All five outbound surfaces (POST, GET SSE, DELETE, send_request_with_headers,
// send_notification) pick up the new token immediately.
client.set_bearer(Some(&fresh_token)).await;
client.disconnect().await?; // DELETE goes out with the fresh bearer
Lifecycle invariants worth knowing (v0.3.33–v0.3.46 hardening):
| Invariant | Notes |
|---|---|
| disconnect() is idempotent | Safe to call multiple times; also fires implicitly from Drop. After explicit disconnect(), the implicit Drop is a no-op (no duplicate DELETE). |
| Concurrent call_tool on Arc<McpClient> runs in parallel | Transport trait takes &self on hot paths; reqwest's connection pool serves concurrent requests. No outer Mutex. |
| SSE GET 4xx is terminal | The background SSE listener treats any 4xx on the GET stream as a permanent failure, clears the cached session ID, and exits. The next connect() re-establishes from a fresh initialize. |
| set_bearer(None) falls back to default headers | Useful for clearing an override and reverting to whatever was baked into ClientBuilder::default_headers(). |
Common rotation pattern for OAuth client_credentials deployments:
loop {
let token = oauth.refresh().await?; // get fresh bearer
client.set_bearer(Some(&token)).await; // swap in place
tokio::time::sleep(token.ttl - skew).await;
}
tool-creation-patterns skilloutput-schemas skillauth-patterns skill for JwtValidator, audience validation, and RFC 9728 metadataauthorization-server-patterns skill for building a test AStools
This skill should be used when the user asks to "create a tool", "add a tool", "new tool", "which tool pattern", "compare tool patterns", "function macro vs derive", "mcp_tool macro", "#[mcp_tool]", "derive McpTool", "#[derive(McpTool)]", "ToolBuilder", "tool creation", "function macro tool", "server icon", "server branding", ".icons()", "Icon::data_uri", "server identity", "dynamic tools", "ToolChangeMode", "activate_tool", "deactivate_tool", "ToolRegistry", "tool_change_mode", or "notifications/tools/list_changed". Covers choosing between function macro (#[mcp_tool]), derive macro (#[derive(McpTool)]), and runtime builder (ToolBuilder) patterns, plus server identity (icons), in the Turul MCP Framework (Rust).
tools
This skill should be used when the user asks about "testing", "test patterns", "write tests", "unit test", "e2e test", "integration test", "McpTestClient", "TestServerManager", "compliance test", "test server", "test fixture", "doctest", "cargo test", "test organization", "SSE testing", or "test consolidation". Covers unit testing, E2E testing, compliance testing, SSE testing, and test organization in the Turul MCP Framework (Rust). McpTestClient is the in-process test harness; for the production client API see mcp-client-patterns.
tools
This skill should be used when the user asks about "task support", "TaskRuntime", "TaskStorage", "task_support attribute", "long-running tool", "CancellationHandle", "tasks/get", "tasks/list", "tasks/cancel", "tasks/result", "TaskStatus", "TaskRecord", "TaskOutcome", "InMemoryTaskStorage", "with_task_storage", "task state machine", "TaskExecutor", "TokioTaskExecutor", "task_support = optional", "task_support = required", "task_support = forbidden", "with_task_runtime", or "task storage backend". Covers MCP task support for long-running tools, state machine, storage backends, cancellation, and capability truthfulness in the Turul MCP Framework (Rust). TaskStorage is distinct from SessionStorage; for session persistence see session-storage-backends.
tools
This skill should be used when the user asks about "session storage", "SessionStorage trait", "SqliteSessionStorage", "PostgresSessionStorage", "DynamoDbSessionStorage", "InMemorySessionStorage", "session backend", "session persistence", "session events", "SSE reconnection storage", "which storage backend", "session TTL", "session cleanup", "session event management", "SseEvent", or "SessionStorageError". Covers the SessionStorage trait, backend selection, event management for SSE resumability, error types, and background cleanup in the Turul MCP Framework (Rust). Do NOT use for TaskStorage — task persistence is a separate trait; see task-patterns.