@@ -7,8 +7,10 @@ package client
77import (
88 "context"
99 "encoding/base64"
10+ "errors"
1011 "fmt"
1112 "io"
13+ "net"
1214 "net/http"
1315
1416 "github.com/mark3labs/mcp-go/client"
@@ -239,6 +241,69 @@ func (h *httpBackendClient) defaultClientFactory(ctx context.Context, target *vm
239241 return c , nil
240242}
241243
244+ // wrapBackendError wraps an error with the appropriate sentinel error based on error type.
245+ // This enables type-safe error checking with errors.Is() instead of string matching.
246+ //
247+ // Error detection strategy (in order of preference):
248+ // 1. Check for standard Go error types (context errors, net.Error, url.Error)
249+ // 2. Fall back to string pattern matching for library-specific errors (MCP SDK, HTTP libs)
250+ //
251+ // Error chain preservation:
252+ // The returned error wraps the sentinel error (ErrTimeout, ErrBackendUnavailable, etc.) with %w
253+ // and formats the original error with %v. This means:
254+ // - errors.Is() works for checking the sentinel error (e.g., errors.Is(err, vmcp.ErrTimeout))
255+ // - errors.As() cannot access the underlying original error type
256+ // This is a deliberate trade-off due to Go's limitation of one %w per fmt.Errorf call.
257+ // If access to the underlying error type is needed in the future, consider implementing
258+ // a custom error type with multiple Unwrap() methods (Go 1.20+).
259+ func wrapBackendError (err error , backendID string , operation string ) error {
260+ if err == nil {
261+ return nil
262+ }
263+
264+ // 1. Type-based detection: Check for context deadline/cancellation
265+ if errors .Is (err , context .DeadlineExceeded ) {
266+ return fmt .Errorf ("%w: failed to %s for backend %s (timeout): %v" ,
267+ vmcp .ErrTimeout , operation , backendID , err )
268+ }
269+ if errors .Is (err , context .Canceled ) {
270+ return fmt .Errorf ("%w: failed to %s for backend %s (cancelled): %v" ,
271+ vmcp .ErrCancelled , operation , backendID , err )
272+ }
273+
274+ // 2. Type-based detection: Check for net.Error with Timeout() method
275+ // This handles network timeouts from the standard library
276+ var netErr net.Error
277+ if errors .As (err , & netErr ) && netErr .Timeout () {
278+ return fmt .Errorf ("%w: failed to %s for backend %s (timeout): %v" ,
279+ vmcp .ErrTimeout , operation , backendID , err )
280+ }
281+
282+ // 3. String-based detection: Fall back to pattern matching for cases where
283+ // we don't have structured error types (MCP SDK, HTTP libraries with embedded status codes)
284+ // Authentication errors (401, 403, auth failures)
285+ if vmcp .IsAuthenticationError (err ) {
286+ return fmt .Errorf ("%w: failed to %s for backend %s: %v" ,
287+ vmcp .ErrAuthenticationFailed , operation , backendID , err )
288+ }
289+
290+ // Timeout errors (deadline exceeded, timeout messages)
291+ if vmcp .IsTimeoutError (err ) {
292+ return fmt .Errorf ("%w: failed to %s for backend %s (timeout): %v" ,
293+ vmcp .ErrTimeout , operation , backendID , err )
294+ }
295+
296+ // Connection errors (refused, reset, unreachable)
297+ if vmcp .IsConnectionError (err ) {
298+ return fmt .Errorf ("%w: failed to %s for backend %s (connection error): %v" ,
299+ vmcp .ErrBackendUnavailable , operation , backendID , err )
300+ }
301+
302+ // Default to backend unavailable for unknown errors
303+ return fmt .Errorf ("%w: failed to %s for backend %s: %v" ,
304+ vmcp .ErrBackendUnavailable , operation , backendID , err )
305+ }
306+
242307// initializeClient performs MCP protocol initialization handshake and returns server capabilities.
243308// This allows the caller to determine which optional features the server supports.
244309func initializeClient (ctx context.Context , c * client.Client ) (* mcp.ServerCapabilities , error ) {
@@ -313,14 +378,14 @@ func (h *httpBackendClient) ListCapabilities(ctx context.Context, target *vmcp.B
313378 // Create a client for this backend (not yet initialized)
314379 c , err := h .clientFactory (ctx , target )
315380 if err != nil {
316- return nil , fmt . Errorf ( "failed to create client for backend %s: %w" , target .WorkloadID , err )
381+ return nil , wrapBackendError ( err , target .WorkloadID , "create client" )
317382 }
318383 defer c .Close ()
319384
320385 // Initialize the client and get server capabilities
321386 serverCaps , err := initializeClient (ctx , c )
322387 if err != nil {
323- return nil , fmt . Errorf ( "failed to initialize client for backend %s: %w" , target .WorkloadID , err )
388+ return nil , wrapBackendError ( err , target .WorkloadID , "initialize client" )
324389 }
325390
326391 logger .Debugf ("Backend %s capabilities: tools=%v, resources=%v, prompts=%v" ,
@@ -330,17 +395,17 @@ func (h *httpBackendClient) ListCapabilities(ctx context.Context, target *vmcp.B
330395 // Check for nil BEFORE passing to functions to avoid interface{} nil pointer issues
331396 toolsResp , err := queryTools (ctx , c , serverCaps .Tools != nil , target .WorkloadID )
332397 if err != nil {
333- return nil , err
398+ return nil , wrapBackendError ( err , target . WorkloadID , "list tools" )
334399 }
335400
336401 resourcesResp , err := queryResources (ctx , c , serverCaps .Resources != nil , target .WorkloadID )
337402 if err != nil {
338- return nil , err
403+ return nil , wrapBackendError ( err , target . WorkloadID , "list resources" )
339404 }
340405
341406 promptsResp , err := queryPrompts (ctx , c , serverCaps .Prompts != nil , target .WorkloadID )
342407 if err != nil {
343- return nil , err
408+ return nil , wrapBackendError ( err , target . WorkloadID , "list prompts" )
344409 }
345410
346411 // Convert MCP types to vmcp types
@@ -428,13 +493,13 @@ func (h *httpBackendClient) CallTool(
428493 // Create a client for this backend
429494 c , err := h .clientFactory (ctx , target )
430495 if err != nil {
431- return nil , fmt . Errorf ( "failed to create client for backend %s: %w" , target .WorkloadID , err )
496+ return nil , wrapBackendError ( err , target .WorkloadID , "create client" )
432497 }
433498 defer c .Close ()
434499
435500 // Initialize the client
436501 if _ , err := initializeClient (ctx , c ); err != nil {
437- return nil , fmt . Errorf ( "failed to initialize client for backend %s: %w" , target .WorkloadID , err )
502+ return nil , wrapBackendError ( err , target .WorkloadID , "initialize client" )
438503 }
439504
440505 // Call the tool using the original capability name from the backend's perspective.
@@ -525,13 +590,13 @@ func (h *httpBackendClient) ReadResource(ctx context.Context, target *vmcp.Backe
525590 // Create a client for this backend
526591 c , err := h .clientFactory (ctx , target )
527592 if err != nil {
528- return nil , fmt . Errorf ( "failed to create client for backend %s: %w" , target .WorkloadID , err )
593+ return nil , wrapBackendError ( err , target .WorkloadID , "create client" )
529594 }
530595 defer c .Close ()
531596
532597 // Initialize the client
533598 if _ , err := initializeClient (ctx , c ); err != nil {
534- return nil , fmt . Errorf ( "failed to initialize client for backend %s: %w" , target .WorkloadID , err )
599+ return nil , wrapBackendError ( err , target .WorkloadID , "initialize client" )
535600 }
536601
537602 // Read the resource using the original URI from the backend's perspective.
@@ -586,13 +651,13 @@ func (h *httpBackendClient) GetPrompt(
586651 // Create a client for this backend
587652 c , err := h .clientFactory (ctx , target )
588653 if err != nil {
589- return "" , fmt . Errorf ( "failed to create client for backend %s: %w" , target .WorkloadID , err )
654+ return "" , wrapBackendError ( err , target .WorkloadID , "create client" )
590655 }
591656 defer c .Close ()
592657
593658 // Initialize the client
594659 if _ , err := initializeClient (ctx , c ); err != nil {
595- return "" , fmt . Errorf ( "failed to initialize client for backend %s: %w" , target .WorkloadID , err )
660+ return "" , wrapBackendError ( err , target .WorkloadID , "initialize client" )
596661 }
597662
598663 // Get the prompt using the original prompt name from the backend's perspective.
0 commit comments