Skip to content
Back to skills

Mcp Client Integration

ASecurity

Integrates MCP clients using Python SDK v2 and TypeScript SDK v2 to connect to MCP servers, manage tools/resources/prompts, handle transport (stdio/SSE), error recovery, and implement structured calling conventions in AI agent applications.

  • 6 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 25, 2026
ai-agentstypescriptpythongoshelltestinggitapidatabasedocumentation

Works with

  • cli
  • api
  • mcp

Security analysis

A100/100

Scanned September 25, 2026

npx -y skills add paulpas/agent-skill-router --skill mcp-client-integration --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Mcp Client Integration?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Mcp Client Integration
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/paulpas-mcp-client-integration/badge)](https://www.skillsdirectory.com/skills/paulpas-mcp-client-integration)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: mcp-client-integration
description: Integrates MCP clients using Python SDK v2 and TypeScript SDK v2 to connect to MCP servers, manage tools/resources/prompts, handle transport (stdio/SSE), error recovery, and implement structured calling conventions in AI agent applications.
license: MIT
compatibility: opencode
metadata:
  version: "1.0.0"
  domain: coding
  role: implementation
  scope: implementation
  output-format: code
  triggers: mcp client, mcp integration, how do i connect to mcp, consuming mcp servers, claude mcp client, typescript mcp, tool invocation
  related-skills: mcp-server-fastmcp-python, mcp-tool-design-patterns
  archetypes: tactical
  anti_triggers: brainstorming, vague ideation
  response_profile:
    verbosity: low
    directive_strength: high
    abstraction_level: operational
---

# MCP Client Integration

Implement MCP clients that connect to MCP servers, discover tools/resources/prompts, handle transport layers (stdio, SSE), and invoke server capabilities with proper error recovery in AI agent applications.

## TL;DR Checklist

- [ ] Choose transport type: stdio (local), SSE (HTTP), or custom connection
- [ ] Initialize client with proper server configuration and timeout settings
- [ ] Implement tool discovery and resource caching mechanisms
- [ ] Handle all error cases: timeouts, malformed responses, tool not found
- [ ] Close connections gracefully on shutdown (context managers / async cleanup)
- [ ] Test tool invocation with realistic error scenarios before production
- [ ] Document required environment variables and server endpoints

---

## When to Use

Use this skill when:

- Building an AI agent that needs to consume tools from external MCP servers
- Integrating Claude SDK with MCP clients for tool discovery and invocation
- Connecting to local stdio-based servers (e.g., filesystem, database servers)
- Consuming HTTP SSE-based MCP servers (streaming transport)
- Implementing fallback/retry logic for unreliable MCP server connections
- Caching tool/resource metadata to reduce server load

---

## When NOT to Use

Avoid this skill for:

- Building an MCP server (use `mcp-server-fastmcp-python` instead)
- One-off tool calls without agent context (use raw HTTP requests)
- Servers that don't implement MCP protocol (use native SDKs)
- Synchronous code that can't handle async/await patterns (refactor to async)
- Simple shell commands or subprocess calls (use `subprocess` module directly)

---

## Core Workflow

### 1. **Initialize Client with Transport**

Choose the appropriate transport layer based on server type:

- **Stdio**: Local in-process or subprocess servers
- **SSE (HTTP)**: Remote servers, streaming responses
- **Custom**: Bidirectional WebSocket or other protocols

**Checkpoint:** Server is running, endpoint/executable is accessible, credentials are configured.

### 2. **Discover Available Capabilities**

List and cache:
- **Tools**: Callable functions with inputs/outputs
- **Resources**: Named data sources (files, databases, API endpoints)
- **Prompts**: Pre-defined prompt templates

**Checkpoint:** Tool catalog is populated, resource URIs are validated.

### 3. **Invoke Tools with Error Handling**

Call tools with proper:
- Type validation for inputs
- Timeout constraints
- Error classification (recoverable vs permanent)
- Retry logic for transient failures

**Checkpoint:** Tool invocation succeeds or raises descriptive error with context.

### 4. **Manage Connection Lifecycle**

Maintain connection with:
- Graceful initialization (handshake, capability negotiation)
- Periodic health checks for long-lived connections
- Proper cleanup on shutdown (close, disconnect)
- Recovery from connection loss

**Checkpoint:** Client can reconnect automatically, resources are freed on exit.

---

## Implementation Patterns

### Pattern 1: Python Client with Stdio Transport

Use this for local MCP servers running as subprocesses (e.g., `mcp-server-filesystem`, `mcp-server-postgres`).

```python
import asyncio
import json
from mcp import ClientSession, StdioServerParameters
from anthropic import Anthropic

class MCPClientManager:
    """Manage MCP client connection and tool invocation."""
    
    def __init__(self, server_path: str, server_args: list = None):
        """Initialize stdio-based MCP client.
        
        Args:
            server_path: Path to MCP server executable
            server_args: Command-line arguments for server
        
        Raises:
            FileNotFoundError: If server executable doesn't exist
            ValueError: If server_path is empty
        """
        if not server_path:
            raise ValueError("server_path cannot be empty")
        
        self.server_path = server_path
        self.server_args = server_args or []
        self.session: ClientSession = None
        self.tools_cache: dict = {}
    
    async def connect(self) -> None:
        """Establish connection to MCP server via stdio.
        
        Raises:
            ConnectionError: If server fails to start or handshake fails
            TimeoutError: If connection takes longer than 10 seconds
        """
        params = StdioServerParameters(
            command=self.server_path,
            args=self.server_args
        )
        
        try:
            self.session = await asyncio.wait_for(
                ClientSession.create(params),
                timeout=10.0
            )
        except asyncio.TimeoutError:
            raise TimeoutError(f"MCP server {self.server_path} failed to start within 10s")
        except Exception as e:
            raise ConnectionError(f"Failed to connect to MCP server: {e}")
    
    async def discover_tools(self) -> list[dict]:
        """Discover available tools from MCP server.
        
        Returns:
            List of tool definitions with name, description, input schema
        
        Raises:
            RuntimeError: If not connected to server
            ValueError: If tool discovery fails
        """
        if not self.session:
            raise RuntimeError("Not connected. Call connect() first.")
        
        try:
            response = await self.session.list_tools()
            self.tools_cache = {tool.name: tool for tool in response.tools}
            return [
                {
                    "name": tool.name,
                    "description": tool.description,
                    "input_schema": tool.inputSchema
                }
                for tool in response.tools
            ]
        except Exception as e:
            raise ValueError(f"Tool discovery failed: {e}")
    
    async def invoke_tool(self, tool_name: str, arguments: dict) -> str:
        """Invoke a tool on the MCP server.
        
        Args:
            tool_name: Name of tool to invoke
            arguments: Input arguments (must match tool's inputSchema)
        
        Returns:
            Tool result as JSON string
        
        Raises:
            ValueError: If tool not found or arguments invalid
            TimeoutError: If invocation exceeds 30 seconds
            RuntimeError: If server returns error response
        """
        if not self.session:
            raise RuntimeError("Not connected. Call connect() first.")
        
        if tool_name not in self.tools_cache:
            raise ValueError(f"Tool '{tool_name}' not found. Available: {list(self.tools_cache.keys())}")
        
        try:
            result = await asyncio.wait_for(
                self.session.call_tool(tool_name, arguments),
                timeout=30.0
            )
            return json.dumps(result.content)
        except asyncio.TimeoutError:
            raise TimeoutError(f"Tool invocation '{tool_name}' exceeded 30s timeout")
        except Exception as e:
            raise RuntimeError(f"Tool invocation failed: {tool_name}: {e}")
    
    async def close(self) -> None:
        """Close MCP session gracefully.
        
        Raises:
            RuntimeError: If close fails (logs error, continues shutdown)
        """
        if self.session:
            try:
                await self.session.close()
            except Exception as e:
                print(f"Warning: Error closing MCP session: {e}")
            finally:
                self.session = None


async def example_usage():
    """Example: Connect to filesystem server and list files."""
    manager = MCPClientManager("/usr/local/bin/mcp-server-filesystem")
    
    try:
        await manager.connect()
        tools = await manager.discover_tools()
        print(f"Available tools: {[t['name'] for t in tools]}")
        
        # Invoke 'list_directory' tool
        result = await manager.invoke_tool(
            "list_directory",
            {"path": "/tmp"}
        )
        print(f"Directory listing: {result}")
    
    finally:
        await manager.close()


# Run example
if __name__ == "__main__":
    asyncio.run(example_usage())
```

---

### Pattern 2: TypeScript Client with Tool Caching and Retry

Use this for integrating MCP clients with Claude SDK in TypeScript agents.

```typescript
import Anthropic from "@anthropic-ai/sdk";

interface ToolDefinition {
  name: string;
  description: string;
  input_schema: Record<string, unknown>;
}

class MCPClientWithRetry {
  private client: Anthropic;
  private toolsCache: Map<string, ToolDefinition> = new Map();
  private maxRetries: number = 3;
  private retryDelayMs: number = 1000;

  constructor(apiKey?: string) {
    this.client = new Anthropic({
      apiKey: apiKey || process.env.ANTHROPIC_API_KEY,
    });
  }

  /**
   * Discover and cache tools from MCP server.
   * Caching reduces server load on repeated agent loops.
   */
  async discoverTools(): Promise<ToolDefinition[]> {
    // In real implementation, fetch from MCP server
    // For demo, return mock tools
    const tools: ToolDefinition[] = [
      {
        name: "get_weather",
        description: "Get current weather for a location",
        input_schema: {
          type: "object",
          properties: {
            location: {
              type: "string",
              description: "City name",
            },
          },
          required: ["location"],
        },
      },
    ];

    tools.forEach((tool) => this.toolsCache.set(tool.name, tool));
    return tools;
  }

  /**
   * Invoke tool with exponential backoff retry logic.
   * Handles transient failures gracefully.
   */
  async invokeTool(
    toolName: string,
    arguments: Record<string, unknown>
  ): Promise<string> {
    if (!this.toolsCache.has(toolName)) {
      throw new Error(
        `Tool '${toolName}' not found. Available: ${Array.from(this.toolsCache.keys()).join(", ")}`
      );
    }

    let lastError: Error | null = null;

    for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
      try {
        // In real implementation, call actual MCP server
        // For demo, simulate tool invocation
        return await this.simulateToolCall(toolName, arguments);
      } catch (error) {
        lastError = error instanceof Error ? error : new Error(String(error));

        // Check if error is retryable
        if (!this.isRetryable(lastError)) {
          throw lastError;
        }

        // Exponential backoff: 1s, 2s, 4s
        if (attempt < this.maxRetries) {
          const delayMs = this.retryDelayMs * Math.pow(2, attempt - 1);
          console.warn(
            `Tool invocation failed (attempt ${attempt}/${this.maxRetries}), retrying in ${delayMs}ms: ${lastError.message}`
          );
          await new Promise((resolve) => setTimeout(resolve, delayMs));
        }
      }
    }

    throw new Error(
      `Tool invocation '${toolName}' failed after ${this.maxRetries} retries: ${lastError?.message}`
    );
  }

  /**
   * Classify errors as retryable (transient) or permanent.
   */
  private isRetryable(error: Error): boolean {
    const message = error.message.toLowerCase();
    return (
      message.includes("timeout") ||
      message.includes("econnrefused") ||
      message.includes("econnreset") ||
      message.includes("503") ||
      message.includes("service unavailable")
    );
  }

  /**
   * Simulate tool call (replace with actual MCP server invocation).
   */
  private async simulateToolCall(
    toolName: string,
    arguments: Record<string, unknown>
  ): Promise<string> {
    // Simulate network call
    await new Promise((resolve) => setTimeout(resolve, 50));
    return JSON.stringify({
      tool: toolName,
      input: arguments,
      result: "Tool executed successfully",
    });
  }

  /**
   * Run agent loop with tool use.
   * Demonstrates integration with Claude SDK.
   */
  async runAgent(userMessage: string): Promise<string> {
    const tools = await this.discoverTools();

    const messages: Anthropic.MessageParam[] = [
      {
        role: "user",
        content: userMessage,
      },
    ];

    // Convert tool definitions to Claude SDK format
    const claudeTools: Anthropic.Tool[] = tools.map((tool) => ({
      name: tool.name,
      description: tool.description,
      input_schema: tool.input_schema,
    }));

    let response = await this.client.messages.create({
      model: "claude-3-5-sonnet-20241022",
      max_tokens: 1024,
      tools: claudeTools,
      messages: messages,
    });

    // Process tool calls in agent loop
    while (response.stop_reason === "tool_use") {
      const toolUseBlock = response.content.find(
        (block) => block.type === "tool_use"
      ) as Anthropic.ToolUseBlock | undefined;

      if (!toolUseBlock) break;

      const toolName = toolUseBlock.name;
      const toolInput = toolUseBlock.input as Record<string, unknown>;

      try {
        const toolResult = await this.invokeTool(toolName, toolInput);

        // Continue conversation with tool result
        messages.push({
          role: "assistant",
          content: response.content,
        });

        messages.push({
          role: "user",
          content: [
            {
              type: "tool_result",
              tool_use_id: toolUseBlock.id,
              content: toolResult,
            },
          ],
        });

        response = await this.client.messages.create({
          model: "claude-3-5-sonnet-20241022",
          max_tokens: 1024,
          tools: claudeTools,
          messages: messages,
        });
      } catch (error) {
        // Return error to Claude
        const errorMsg = error instanceof Error ? error.message : String(error);
        messages.push({
          role: "user",
          content: [
            {
              type: "tool_result",
              tool_use_id: toolUseBlock.id,
              is_error: true,
              content: errorMsg,
            },
          ],
        });

        response = await this.client.messages.create({
          model: "claude-3-5-sonnet-20241022",
          max_tokens: 1024,
          tools: claudeTools,
          messages: messages,
        });
      }
    }

    // Extract final text response
    const textBlock = response.content.find((block) => block.type === "text");
    return textBlock && "text" in textBlock ? textBlock.text : "";
  }
}

// Usage
const client = new MCPClientWithRetry();
client.runAgent("What's the weather in San Francisco?");
```

---

### Pattern 3: BAD vs GOOD Error Handling

#### ❌ BAD: Silent Failures and Unclear Recovery

```python
async def invoke_tool_bad(tool_name: str, args: dict) -> dict:
    """Dangerous: Silently fails, no clear error context."""
    try:
        result = await session.call_tool(tool_name, args)
        return result.content
    except:
        # What error? What tool failed? No context.
        return {}  # Silent failure — caller has no idea what went wrong
    
    # No timeout protection → can hang indefinitely
    # No retry logic → transient failures break the agent
    # No validation → malformed responses corrupt downstream logic
```

**Problems:**
- Empty dict is indistinguishable from successful "no result" case
- Caller has no information about what failed or why
- No timeout means a slow/hanging server blocks the entire agent
- No retry for transient failures (network hiccups, server restart)

#### ✅ GOOD: Explicit Error Types and Context

```python
class ToolInvocationError(Exception):
    """Base exception for tool invocation failures."""
    def __init__(self, tool_name: str, reason: str, is_retryable: bool = False):
        self.tool_name = tool_name
        self.reason = reason
        self.is_retryable = is_retryable
        super().__init__(f"Tool '{tool_name}' failed: {reason}")


async def invoke_tool_good(
    tool_name: str,
    arguments: dict,
    max_retries: int = 3,
    timeout_seconds: float = 30.0
) -> dict:
    """Correct: Clear errors, retry logic, timeout protection.
    
    Args:
        tool_name: Name of tool to invoke
        arguments: Input arguments for tool
        max_retries: Number of retries for transient errors
        timeout_seconds: Maximum time for single invocation
    
    Returns:
        Tool result dictionary
    
    Raises:
        ToolInvocationError: With clear reason and retryability flag
        ValueError: If arguments don't match tool schema
    """
    if tool_name not in tools_cache:
        raise ValueError(
            f"Tool '{tool_name}' not found in available tools: {list(tools_cache.keys())}"
        )
    
    last_error = None
    
    for attempt in range(1, max_retries + 1):
        try:
            # Timeout protection: prevents hanging on slow servers
            result = await asyncio.wait_for(
                session.call_tool(tool_name, arguments),
                timeout=timeout_seconds
            )
            
            # Validate response structure
            if not hasattr(result, "content"):
                raise ToolInvocationError(
                    tool_name,
                    "Malformed response: missing 'content' field",
                    is_retryable=False
                )
            
            return result.content
        
        except asyncio.TimeoutError:
            last_error = ToolInvocationError(
                tool_name,
                f"Timeout after {timeout_seconds}s",
                is_retryable=True
            )
        except ConnectionError as e:
            last_error = ToolInvocationError(
                tool_name,
                f"Connection error: {e}",
                is_retryable=True
            )
        except ValueError as e:
            # Schema validation error — not retryable
            raise ToolInvocationError(
                tool_name,
                f"Invalid arguments: {e}",
                is_retryable=False
            )
        except Exception as e:
            last_error = ToolInvocationError(
                tool_name,
                f"Unexpected error: {type(e).__name__}: {e}",
                is_retryable=False
            )
        
        # If not retryable, fail immediately
        if last_error and not last_error.is_retryable:
            raise last_error
        
        # Exponential backoff before retry
        if attempt < max_retries:
            backoff_seconds = 2 ** (attempt - 1)  # 1s, 2s, 4s, ...
            print(f"Attempt {attempt}/{max_retries} failed, retrying in {backoff_seconds}s: {last_error.reason}")
            await asyncio.sleep(backoff_seconds)
    
    # All retries exhausted
    raise last_error or ToolInvocationError(
        tool_name,
        f"Failed after {max_retries} retries",
        is_retryable=False
    )


# Usage with proper error handling
try:
    result = await invoke_tool_good("list_files", {"directory": "/tmp"})
    print(f"Success: {result}")
except ToolInvocationError as e:
    if e.is_retryable:
        print(f"Transient error (safe to retry): {e.reason}")
        # Agent can decide to retry or backoff
    else:
        print(f"Permanent error (don't retry): {e.reason}")
        # Agent should fail immediately
```

**Improvements:**
- Explicit error types with `is_retryable` flag for agent decision-making
- Timeout protection prevents hanging
- Exponential backoff reduces server load on transient failures
- Clear context in error messages (tool name, reason, suggestion)
- Validates response structure before returning
- Distinguishes transient (timeout, connection) from permanent (validation, not found) errors

---

## Error Recovery Patterns

### Handling Timeouts

```python
import asyncio

async def invoke_with_timeout(tool_name: str, args: dict, timeout_sec: float = 30.0) -> dict:
    """Invoke tool with timeout and graceful timeout handling."""
    try:
        result = await asyncio.wait_for(
            session.call_tool(tool_name, args),
            timeout=timeout_sec
        )
        return result.content
    except asyncio.TimeoutError:
        raise TimeoutError(
            f"Tool '{tool_name}' did not respond within {timeout_sec}s. "
            f"Server may be overloaded or hung. Consider increasing timeout or retrying."
        )
```

### Handling Tool Not Found

```python
async def safe_invoke_tool(tool_name: str, args: dict) -> dict:
    """Invoke tool with validation that it exists first."""
    available_tools = {t.name for t in (await session.list_tools()).tools}
    
    if tool_name not in available_tools:
        raise ValueError(
            f"Tool '{tool_name}' not found. Available tools: {', '.join(sorted(available_tools))}"
        )
    
    return (await session.call_tool(tool_name, args)).content
```

### Handling Malformed Responses

```python
import json

async def invoke_with_validation(tool_name: str, args: dict) -> dict:
    """Invoke tool and validate response structure."""
    result = await session.call_tool(tool_name, args)
    
    # Validate response has required fields
    if not isinstance(result.content, (list, dict, str)):
        raise ValueError(
            f"Malformed response from '{tool_name}': "
            f"expected dict/list/str, got {type(result.content).__name__}"
        )
    
    # Try to parse if JSON string
    if isinstance(result.content, str):
        try:
            return json.loads(result.content)
        except json.JSONDecodeError as e:
            raise ValueError(
                f"Tool '{tool_name}' returned invalid JSON: {e}"
            )
    
    return result.content
```

---

## Transport Layer Details

### Stdio Transport (Local Servers)

Best for:
- Local development
- Private MCP servers (filesystem, database)
- Low-latency requirements
- Subprocess-managed servers

```python
from mcp import StdioServerParameters, ClientSession

# Launch server as subprocess
params = StdioServerParameters(
    command="/usr/local/bin/mcp-server-postgres",
    args=["--database", "production"]
)

session = await ClientSession.create(params)
```

**Advantages:** Direct process control, low latency, secure (no network)  
**Disadvantages:** Server must run locally, subprocess management overhead

### SSE Transport (HTTP Streaming)

Best for:
- Remote servers (cloud deployments)
- Firewall-friendly (HTTP only)
- Multi-tenant architectures
- Load-balanced server instances

```python
from mcp import ServerParameters
import aiohttp

# Connect to HTTP SSE server
async with aiohttp.ClientSession() as http_session:
    params = ServerParameters(
        url="https://mcp-server.example.com/sse",
        headers={"Authorization": f"Bearer {api_key}"}
    )
    session = await ClientSession.create(params)
```

**Advantages:** Remote access, scalable, firewall-friendly  
**Disadvantages:** Network latency, requires server deployment

---

## Connection Lifecycle Management

### Proper Initialization (Handshake)

```python
async def connect_and_verify(server_path: str) -> ClientSession:
    """Connect with verification that server is responsive."""
    session = await ClientSession.create(StdioServerParameters(command=server_path))
    
    try:
        # Verify server is responsive by listing tools
        tools_response = await asyncio.wait_for(
            session.list_tools(),
            timeout=5.0
        )
        print(f"Connected. Available tools: {[t.name for t in tools_response.tools]}")
        return session
    except Exception as e:
        await session.close()
        raise ConnectionError(f"Server verification failed: {e}")
```

### Graceful Shutdown

```python
async def shutdown_client(session: ClientSession) -> None:
    """Close connection and cleanup resources."""
    try:
        await session.close()
        print("Client closed gracefully")
    except Exception as e:
        print(f"Warning: Error closing client: {e}")
    finally:
        # Ensure cleanup even if close() fails
        session = None
```

### Context Manager Pattern (Recommended)

```python
from contextlib import asynccontextmanager

@asynccontextmanager
async def mcp_client(server_path: str):
    """Context manager ensures proper cleanup."""
    session = None
    try:
        session = await ClientSession.create(StdioServerParameters(command=server_path))
        yield session
    finally:
        if session:
            await session.close()

# Usage
async with mcp_client("/usr/local/bin/mcp-server-filesystem") as session:
    tools = await session.list_tools()
    # Do work...
# Session automatically closed when exiting block
```

---

## Best Practices

### 1. Cache Tool Metadata

```python
class MCPClient:
    def __init__(self):
        self.tools_cache: dict = {}
        self.cache_timestamp = None
        self.cache_ttl_seconds = 3600  # Refresh every hour
    
    async def get_tools(self, force_refresh: bool = False) -> list:
        """Get tools with caching to reduce server load."""
        import time
        
        now = time.time()
        should_refresh = (
            force_refresh or
            not self.tools_cache or
            (self.cache_timestamp and now - self.cache_timestamp > self.cache_ttl_seconds)
        )
        
        if should_refresh:
            tools = await self.session.list_tools()
            self.tools_cache = {t.name: t for t in tools.tools}
            self.cache_timestamp = now
        
        return list(self.tools_cache.values())
```

### 2. Implement Health Checks

```python
async def health_check(session: ClientSession, timeout_sec: float = 5.0) -> bool:
    """Quick check that server is responsive."""
    try:
        await asyncio.wait_for(
            session.list_tools(),
            timeout=timeout_sec
        )
        return True
    except Exception:
        return False
```

### 3. Type-Safe Tool Invocation

```python
from typing import TypedDict

class WeatherInput(TypedDict):
    location: str
    unit: str  # "celsius" or "fahrenheit"

async def get_weather(args: WeatherInput) -> dict:
    """Type-safe wrapper for weather tool."""
    return await invoke_tool("get_weather", args)
```

---

## Constraints

### MUST DO

- **Always use context managers or async cleanup** for connection lifecycle management
- **Implement timeout protection** for all tool invocations (prevent hanging)
- **Classify errors as retryable vs permanent** to guide agent behavior
- **Cache tool/resource metadata** to reduce server load
- **Validate tool response structure** before using results
- **Log clear error messages** with tool name, reason, and recovery suggestion
- **Test with realistic error scenarios** (timeouts, missing tools, malformed responses) before production
- **Use exponential backoff** for retrying transient failures (1s, 2s, 4s, ...)
- **Document required environment variables** and server endpoint configuration
- **Handle connection loss gracefully** with automatic reconnection for long-lived clients

---

### MUST NOT DO

- **Do NOT silently fail** with empty results or default values — throw descriptive errors
- **Do NOT use generic exception catching** without classifying error type
- **Do NOT retry permanent errors** (invalid arguments, tool not found) — waste time and resources
- **Do NOT invoke unbounded retries** — cap at 3-5 retries with backoff
- **Do NOT block on tool invocation** without timeout — can hang the entire agent
- **Do NOT assume tools are cached** — call list_tools() if unsure
- **Do NOT parse malformed tool responses** without validation — corrupts downstream logic
- **Do NOT share client sessions** across async tasks without synchronization
- **Do NOT hardcode server paths/URLs** — use environment variables and config files
- **Do NOT leave connections open** on shutdown — implement proper cleanup

---

## Resource Discovery and Management

### Listing Resources

```python
async def discover_resources(session: ClientSession) -> dict[str, list]:
    """Discover and categorize available resources."""
    response = await session.list_resources()
    
    resources_by_type = {}
    for resource in response.resources:
        resource_type = resource.uri.split("://")[0]  # e.g., "file", "database"
        if resource_type not in resources_by_type:
            resources_by_type[resource_type] = []
        resources_by_type[resource_type].append({
            "uri": resource.uri,
            "name": resource.name,
            "description": resource.description,
            "mime_type": resource.mimeType
        })
    
    return resources_by_type
```

### Reading Resource Content

```python
async def read_resource(session: ClientSession, resource_uri: str) -> str:
    """Read content from a resource (file, database query, API response)."""
    try:
        response = await asyncio.wait_for(
            session.read_resource(resource_uri),
            timeout=10.0
        )
        
        # Handle different response types
        if isinstance(response.contents, str):
            return response.contents
        elif isinstance(response.contents, bytes):
            return response.contents.decode("utf-8")
        else:
            return str(response.contents)
    
    except asyncio.TimeoutError:
        raise TimeoutError(f"Reading resource '{resource_uri}' exceeded timeout")
    except Exception as e:
        raise RuntimeError(f"Failed to read resource '{resource_uri}': {e}")
```

---

## Testing Integration

### Unit Test for Tool Invocation

```python
import pytest
from unittest.mock import AsyncMock, MagicMock

@pytest.mark.asyncio
async def test_invoke_tool_success():
    """Test successful tool invocation."""
    # Mock session
    mock_session = AsyncMock()
    mock_response = MagicMock()
    mock_response.content = {"result": "success"}
    mock_session.call_tool.return_value = mock_response
    
    # Test invocation
    result = await invoke_tool_good("test_tool", {"arg": "value"})
    assert result == {"result": "success"}
    mock_session.call_tool.assert_called_once_with("test_tool", {"arg": "value"})


@pytest.mark.asyncio
async def test_invoke_tool_not_found():
    """Test error when tool doesn't exist."""
    with pytest.raises(ValueError, match="Tool 'missing_tool' not found"):
        await invoke_tool_good("missing_tool", {})


@pytest.mark.asyncio
async def test_invoke_tool_timeout():
    """Test timeout handling and retry logic."""
    mock_session = AsyncMock()
    mock_session.call_tool.side_effect = asyncio.TimeoutError()
    
    with pytest.raises(ToolInvocationError) as exc_info:
        await invoke_tool_good("slow_tool", {}, max_retries=1, timeout_seconds=0.1)
    
    assert "Timeout" in str(exc_info.value)
```

---

## Common Integration Patterns

### Pattern: Agent with Tool Use Loop

```python
async def agent_loop_with_tools(user_message: str, max_iterations: int = 10) -> str:
    """Run agent loop that invokes tools until completion."""
    messages = [{"role": "user", "content": user_message}]
    
    for iteration in range(max_iterations):
        # Get Claude response
        response = await client.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=1024,
            tools=[tool.to_claude_format() for tool in available_tools],
            messages=messages
        )
        
        if response.stop_reason == "end_turn":
            # Claude finished
            return extract_text(response)
        
        if response.stop_reason == "tool_use":
            # Process tool calls
            for block in response.content:
                if block.type == "tool_use":
                    try:
                        result = await invoke_tool_good(block.name, block.input)
                        messages.append({"role": "assistant", "content": response.content})
                        messages.append({
                            "role": "user",
                            "content": [{
                                "type": "tool_result",
                                "tool_use_id": block.id,
                                "content": json.dumps(result)
                            }]
                        })
                    except ToolInvocationError as e:
                        # Return error to Claude
                        messages.append({
                            "role": "user",
                            "content": [{
                                "type": "tool_result",
                                "tool_use_id": block.id,
                                "is_error": True,
                                "content": e.reason
                            }]
                        })
    
    return "Max iterations reached"
```

---

## Related Documentation

- **MCP Protocol Specification**: https://modelcontextprotocol.io
- **Claude SDK for Python**: https://github.com/anthropics/anthropic-sdk-python
- **Claude SDK for TypeScript**: https://github.com/anthropics/anthropic-sdk-typescript
- **MCP Server Pattern**: See `mcp-server-fastmcp-python` skill
- **Tool Design**: See `mcp-tool-design-patterns` skill

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…