Skip to content

Commit b0a7c45

Browse files
committed
Implement enhanced error handling and logging for tool calls in MCPProxyServer and Client
- Introduced createDetailedErrorResponse method to generate enriched error responses with HTTP and troubleshooting context. - Added extractStatusCodeFromError and generateTroubleshootingAdvice methods for improved error classification and guidance. - Enhanced Client struct to extract and log detailed HTTP and JSON-RPC error information during tool calls. - Updated error handling in CallTool methods to provide clearer context and recovery instructions for various error scenarios.
1 parent 9ef6e50 commit b0a7c45

File tree

3 files changed

+382
-22
lines changed

3 files changed

+382
-22
lines changed

internal/server/mcp.go

Lines changed: 154 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,25 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"regexp"
8+
"strconv"
79
"strings"
810
"time"
911

10-
"github.com/mark3labs/mcp-go/mcp"
11-
mcpserver "github.com/mark3labs/mcp-go/server"
12-
"go.uber.org/zap"
13-
1412
"mcpproxy-go/internal/cache"
1513
"mcpproxy-go/internal/config"
1614
"mcpproxy-go/internal/index"
1715
"mcpproxy-go/internal/logs"
1816
"mcpproxy-go/internal/storage"
17+
"mcpproxy-go/internal/transport"
1918
"mcpproxy-go/internal/truncate"
2019
"mcpproxy-go/internal/upstream"
20+
21+
"errors"
22+
23+
"github.com/mark3labs/mcp-go/mcp"
24+
mcpserver "github.com/mark3labs/mcp-go/server"
25+
"go.uber.org/zap"
2126
)
2227

2328
const (
@@ -430,7 +435,7 @@ func (p *MCPProxyServer) handleCallTool(ctx context.Context, request mcp.CallToo
430435
zap.String("server_name", serverName),
431436
zap.String("actual_tool", actualToolName))
432437

433-
return mcp.NewToolResultError(err.Error()), nil
438+
return p.createDetailedErrorResponse(err, serverName, actualToolName), nil
434439
}
435440

436441
// Increment usage stats
@@ -1398,6 +1403,150 @@ func (p *MCPProxyServer) handleTailLog(_ context.Context, request mcp.CallToolRe
13981403
return mcp.NewToolResultText(string(jsonResult)), nil
13991404
}
14001405

1406+
// createDetailedErrorResponse creates an enhanced error response with HTTP and troubleshooting context
1407+
func (p *MCPProxyServer) createDetailedErrorResponse(err error, serverName, toolName string) *mcp.CallToolResult {
1408+
// Try to extract HTTP error details
1409+
var httpErr *transport.HTTPError
1410+
var jsonRPCErr *transport.JSONRPCError
1411+
1412+
// Check if it's our enhanced error types
1413+
if errors.As(err, &httpErr) {
1414+
// We have HTTP error details
1415+
errorDetails := map[string]interface{}{
1416+
"error": httpErr.Error(),
1417+
"http_details": map[string]interface{}{
1418+
"status_code": httpErr.StatusCode,
1419+
"response_body": httpErr.Body,
1420+
"server_url": httpErr.URL,
1421+
"method": httpErr.Method,
1422+
},
1423+
"troubleshooting": p.generateTroubleshootingAdvice(httpErr.StatusCode, httpErr.Body),
1424+
}
1425+
1426+
jsonResponse, _ := json.Marshal(errorDetails)
1427+
return mcp.NewToolResultError(string(jsonResponse))
1428+
}
1429+
1430+
if errors.As(err, &jsonRPCErr) {
1431+
// We have JSON-RPC error details
1432+
errorDetails := map[string]interface{}{
1433+
"error": jsonRPCErr.Message,
1434+
"error_code": jsonRPCErr.Code,
1435+
"error_data": jsonRPCErr.Data,
1436+
}
1437+
1438+
if jsonRPCErr.HTTPError != nil {
1439+
errorDetails["http_details"] = map[string]interface{}{
1440+
"status_code": jsonRPCErr.HTTPError.StatusCode,
1441+
"response_body": jsonRPCErr.HTTPError.Body,
1442+
"server_url": jsonRPCErr.HTTPError.URL,
1443+
}
1444+
errorDetails["troubleshooting"] = p.generateTroubleshootingAdvice(jsonRPCErr.HTTPError.StatusCode, jsonRPCErr.HTTPError.Body)
1445+
}
1446+
1447+
jsonResponse, _ := json.Marshal(errorDetails)
1448+
return mcp.NewToolResultError(string(jsonResponse))
1449+
}
1450+
1451+
// Extract status codes and helpful info from error message for enhanced responses
1452+
errStr := err.Error()
1453+
if strings.Contains(errStr, "status code") || strings.Contains(errStr, "HTTP") {
1454+
// Try to extract HTTP status code for troubleshooting advice
1455+
statusCode := p.extractStatusCodeFromError(errStr)
1456+
1457+
errorDetails := map[string]interface{}{
1458+
"error": errStr,
1459+
"server_name": serverName,
1460+
"tool_name": toolName,
1461+
}
1462+
1463+
if statusCode > 0 {
1464+
errorDetails["http_status"] = statusCode
1465+
errorDetails["troubleshooting"] = p.generateTroubleshootingAdvice(statusCode, errStr)
1466+
}
1467+
1468+
jsonResponse, _ := json.Marshal(errorDetails)
1469+
return mcp.NewToolResultError(string(jsonResponse))
1470+
}
1471+
1472+
// Fallback to enhanced error message
1473+
errorDetails := map[string]interface{}{
1474+
"error": errStr,
1475+
"server_name": serverName,
1476+
"tool_name": toolName,
1477+
"troubleshooting": "Check server configuration, connectivity, and authentication credentials",
1478+
}
1479+
1480+
jsonResponse, _ := json.Marshal(errorDetails)
1481+
return mcp.NewToolResultError(string(jsonResponse))
1482+
}
1483+
1484+
// extractStatusCodeFromError attempts to extract HTTP status code from error message
1485+
func (p *MCPProxyServer) extractStatusCodeFromError(errStr string) int {
1486+
// Common patterns for status codes in error messages
1487+
patterns := []string{
1488+
`status code (\d+)`,
1489+
`HTTP (\d+)`,
1490+
`(\d+) [A-Za-z\s]+$`, // "400 Bad Request" pattern
1491+
}
1492+
1493+
for _, pattern := range patterns {
1494+
if matches := regexp.MustCompile(pattern).FindStringSubmatch(errStr); len(matches) > 1 {
1495+
if code, err := strconv.Atoi(matches[1]); err == nil {
1496+
return code
1497+
}
1498+
}
1499+
}
1500+
1501+
return 0
1502+
}
1503+
1504+
// generateTroubleshootingAdvice provides specific troubleshooting advice based on HTTP status codes and error content
1505+
func (p *MCPProxyServer) generateTroubleshootingAdvice(statusCode int, errorBody string) string {
1506+
switch statusCode {
1507+
case 400:
1508+
if strings.Contains(strings.ToLower(errorBody), "api key") || strings.Contains(strings.ToLower(errorBody), "key") {
1509+
return "Check API key configuration. Ensure the API key is correctly set in server environment variables or configuration."
1510+
}
1511+
if strings.Contains(strings.ToLower(errorBody), "auth") {
1512+
return "Authentication issue. Verify authentication credentials and configuration."
1513+
}
1514+
return "Bad request. Check tool parameters, API endpoint configuration, and request format."
1515+
1516+
case 401:
1517+
return "Authentication required. Check API keys, tokens, or authentication credentials in server configuration."
1518+
1519+
case 403:
1520+
return "Access forbidden. Verify API key permissions, user authorization, or check if the service requires additional authentication."
1521+
1522+
case 404:
1523+
return "Resource not found. Check API endpoint URL, server configuration, or verify the requested resource exists."
1524+
1525+
case 429:
1526+
return "Rate limit exceeded. Wait before retrying or check if you need a higher rate limit plan."
1527+
1528+
case 500:
1529+
return "Internal server error. The upstream service is experiencing issues. Try again later or contact the service provider."
1530+
1531+
case 502, 503, 504:
1532+
return "Service unavailable or timeout. The upstream service may be down or overloaded. Check service status and try again later."
1533+
1534+
default:
1535+
if strings.Contains(strings.ToLower(errorBody), "api key") {
1536+
return "API key issue detected. Check environment variables and server configuration for correct API key setup."
1537+
}
1538+
if strings.Contains(strings.ToLower(errorBody), "timeout") {
1539+
return "Request timeout. The server may be slow or overloaded. Check network connectivity and server responsiveness."
1540+
}
1541+
if strings.Contains(strings.ToLower(errorBody), "connection") {
1542+
return "Connection issue. Check network connectivity, server URL, and firewall settings."
1543+
}
1544+
return "Check server configuration, network connectivity, and authentication settings. Review server logs for more details."
1545+
}
1546+
}
1547+
1548+
// getServerErrorContext extracts relevant context information for error reporting
1549+
14011550
// GetMCPServer returns the underlying MCP server for serving
14021551
func (p *MCPProxyServer) GetMCPServer() *mcpserver.MCPServer {
14031552
return p.server

internal/transport/http.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package transport
22

33
import (
44
"fmt"
5+
"net/http"
56

67
"mcpproxy-go/internal/config"
78

@@ -17,6 +18,69 @@ const (
1718
TransportStdio = "stdio"
1819
)
1920

21+
// HTTPError represents detailed HTTP error information for debugging
22+
type HTTPError struct {
23+
StatusCode int `json:"status_code"`
24+
Headers map[string]string `json:"headers"`
25+
Body string `json:"body"`
26+
Method string `json:"method"`
27+
URL string `json:"url"`
28+
Err error `json:"-"` // Original error
29+
}
30+
31+
func (e *HTTPError) Error() string {
32+
if e.Body != "" {
33+
return fmt.Sprintf("HTTP %d %s: %s", e.StatusCode, http.StatusText(e.StatusCode), e.Body)
34+
}
35+
return fmt.Sprintf("HTTP %d %s", e.StatusCode, http.StatusText(e.StatusCode))
36+
}
37+
38+
// JSONRPCError represents JSON-RPC specific error information
39+
type JSONRPCError struct {
40+
Code int `json:"code"`
41+
Message string `json:"message"`
42+
Data interface{} `json:"data,omitempty"`
43+
HTTPError *HTTPError `json:"http_error,omitempty"`
44+
}
45+
46+
func (e *JSONRPCError) Error() string {
47+
if e.HTTPError != nil {
48+
return fmt.Sprintf("JSON-RPC Error %d: %s (HTTP: %s)", e.Code, e.Message, e.HTTPError.Error())
49+
}
50+
return fmt.Sprintf("JSON-RPC Error %d: %s", e.Code, e.Message)
51+
}
52+
53+
// HTTPResponseDetails captures detailed HTTP response information for debugging
54+
type HTTPResponseDetails struct {
55+
StatusCode int `json:"status_code"`
56+
Headers map[string]string `json:"headers"`
57+
Body string `json:"body"`
58+
URL string `json:"url"`
59+
Method string `json:"method"`
60+
}
61+
62+
// EnhancedHTTPError creates an HTTPError with full context
63+
func NewHTTPError(statusCode int, body, method, url string, headers map[string]string, originalErr error) *HTTPError {
64+
return &HTTPError{
65+
StatusCode: statusCode,
66+
Headers: headers,
67+
Body: body,
68+
Method: method,
69+
URL: url,
70+
Err: originalErr,
71+
}
72+
}
73+
74+
// NewJSONRPCError creates a JSONRPCError with optional HTTP context
75+
func NewJSONRPCError(code int, message string, data interface{}, httpErr *HTTPError) *JSONRPCError {
76+
return &JSONRPCError{
77+
Code: code,
78+
Message: message,
79+
Data: data,
80+
HTTPError: httpErr,
81+
}
82+
}
83+
2084
// HTTPTransportConfig holds configuration for HTTP transport
2185
type HTTPTransportConfig struct {
2286
URL string

0 commit comments

Comments
 (0)