Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsBlogPro
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges
  • Chrome Extension
  • Skill Manager

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
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 views
Added 9/25/2026
ai-agentstypescriptpythongoshelltestinggitapidatabasedocumentation

Works with

cliapimcp

Security Analysis

A100/100

Scanned 9/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 automatically 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
Files
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

paulpaspaulpas
View sourceSee grades on GitHubMore from paulpas →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

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 (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Caveman

Terse caveman voice: answer first, fluff gone, every technical fact kept. Use for /caveman, "caveman mode", "talk like caveman", "be brief", "less tokens". Stays on until "stop caveman" or "normal mode".

1100021 votes

Hyperplan

Adversarial multi-agent planning skill. Self-orchestrates 5 hostile category members (unspecified-low, unspecified-high, deep, ultrabrain, artistry) via team-mode for ruthless cross-critique debate, distills only the defensible insights, then MANDATORILY hands the distilled insight bundle to the `plan` agent for executable plan formalization. Use when planning needs maximum rigor and surfacing of weak assumptions, blind spots, and over-engineering. Triggers: 'hyperplan', 'hpp', '/hyperplan', ...

698431 votes

Writing Skills

Create and manage Claude Code skills in HASH repository following Anthropic best practices. Use when creating new skills, modifying skill-rules.json, understanding trigger patterns, working with hooks, debugging skill activation, or implementing progressive disclosure. Covers skill structure, YAML frontmatter, trigger types (keywords, intent patterns), UserPromptSubmit hook, and the 500-line rule. Includes validation and debugging with SKILL_DEBUG. Examples include rust-error-stack, cargo-dep...

3931 votes

Mcp Code Execution

Routes multi-tool workflows through MCP servers for large datasets and pipelines. Use when Bash tool overhead is limiting throughput on data-heavy tasks.

3421 votes

catchup

Recovers the conversation and failed tool calls of a previous Codex, Amp, Claude Code, Antigravity, Cline, Copilot CLI, Cursor, DeepSeek Harness, Grok Build, Kimi, OpenCode, Pi Agent, or ZCode session. Use when the user says "catch up", "what did the last session do", "get me up to speed", "I switched agents", asks to recover/summarize a previous session before continuing, or asks to diagnose or report a catchup failure. Do NOT use for the current conversation, git history, or any non-agent log.

741 votes
View all in ai-agents →