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 Tool Design Patterns

ASecurity

Designs effective MCP tools and resources following best practices including clear descriptions, bounded inputs/outputs, proper annotations (readOnlyHint, destructiveHint, idempotentHint), hierarchical resource templates, progressive discovery patterns, and avoiding anti-patterns like tool bloat, vague contracts, and unbounded responses.

6 stars
0 votes
0 copies
1 views
Added 9/25/2026
businesspythongodebuggingapidatabaseperformancedocumentation

Works with

cursorterminalcliapimcp

Security Analysis

A100/100

Scanned 9/25/2026

$npx -y skills add paulpas/agent-skill-router --skill mcp-tool-design-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Mcp Tool Design Patterns?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Mcp Tool Design Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/paulpas-mcp-tool-design-patterns/badge)](https://www.skillsdirectory.com/skills/paulpas-mcp-tool-design-patterns)

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-tool-design-patterns
description: Designs effective MCP tools and resources following best practices including clear descriptions, bounded inputs/outputs, proper annotations (readOnlyHint, destructiveHint, idempotentHint), hierarchical resource templates, progressive discovery patterns, and avoiding anti-patterns like tool bloat, vague contracts, and unbounded responses.
license: MIT
compatibility: opencode
metadata:
  version: "1.0.0"
  domain: coding
  role: implementation
  scope: implementation
  output-format: code
  triggers: mcp tool design, resource design, tool descriptions, mcp schema, idempotent hint, resource templates, bounded responses
  related-skills: mcp-server-fastmcp-python, mcp-client-integration
  archetypes: tactical, strategic
  anti_triggers: brainstorming, vague ideation
  response_profile:
    verbosity: medium
    directive_strength: high
    abstraction_level: operational
---

# MCP Tool Design Patterns

Designs effective Model Context Protocol (MCP) tools and resources that enable AI models to interact with external systems reliably and safely.

## TL;DR Checklist

- [ ] **Progressive Discovery:** Core tools only (5–10), add depth via resources and templates
- [ ] **Strict Schemas:** Use Pydantic models with constraints, not open dicts
- [ ] **Clear Descriptions:** Every tool and resource must be unambiguous (not "Get data")
- [ ] **Bounded Responses:** Paginate lists, limit array sizes, define max response sizes
- [ ] **Annotations:** Mark tools with readOnlyHint, destructiveHint, idempotentHint where applicable
- [ ] **Hierarchical Resources:** Organize by parent/child relationships (DNS-like URIs)
- [ ] **Avoid Tool Bloat:** > 15 tools reduces model accuracy; use resources instead

---

## When to Use

Use this skill when:

- Designing a new MCP server with tools and resources
- Creating tool schemas that AI models will call
- Building resource hierarchies for progressive discovery
- Reviewing existing tools for anti-patterns
- Planning how to expose system functionality via MCP
- Setting up pagination, filtering, or streaming patterns

---

## When NOT to Use

Avoid this skill for:

- Implementing MCP client-side integration (use `mcp-client-integration` instead)
- Setting up MCP server frameworks (use `mcp-server-fastmcp-python` instead)
- Debugging protocol-level MCP issues
- Tasks that don't involve tool/resource design

---

## Core Concepts

### Tool Anatomy

Every MCP tool is a callable unit of work with:

1. **Name** — Lowercase, kebab-case, descriptive (not `get_stuff`, use `fetch-customer-invoice`)
2. **Description** — 1–2 sentences, specific and unambiguous
3. **Input Schema** — Pydantic BaseModel with strict field constraints
4. **Annotations** — Hints about tool behavior (readOnly, destructive, idempotent)
5. **Output** — Structured, bounded response (never unbounded arrays)

### Resource Anatomy

Resources expose data that can be accessed via URIs:

1. **URI Template** — Hierarchical path like `dns://api.example.com/users/{id}/settings`
2. **Description** — What the resource exposes and how to filter/paginate
3. **MIME Type** — Expected format (`text/plain`, `application/json`, `text/html`)
4. **Read-Only Hint** — Indicates if resource can be modified

### Tool vs Resource Decision Tree

```
Does it accept input and perform an action?
  → Tool (call `search-users`, `send-email`, `create-invoice`)

Does it represent structured data accessible via a URI?
  → Resource (access `dns://api.example.com/users/{id}`)

Is it mostly discovery (listing possibilities)?
  → Resource template (let client explore `dns://api.example.com/users`)

Is it a long-running operation?
  → Tool (tools can include status polling hints)

Should the model explore it progressively?
  → Resource + Resource Template (start with core tools, add depth via templates)
```

---

## Design Pattern 1: Progressive Discovery

Start with 5–10 core tools. Use resources and templates to enable the model to discover additional functionality as needed.

**Why?** AI models perform better with focused tool sets. Too many tools → lower accuracy and higher latency.

### Pattern Structure

```
Core Tools (5–10)
  ↓
  Resource Templates
    ↓
    (Model explores templates as needed)
```

### Example: Customer Management System

**Core Tools:**
- `search-customers` — Find customers by name or ID
- `fetch-customer-details` — Get full profile for a specific customer
- `create-invoice` — Generate an invoice
- `send-email` — Send notification emails

**Resource Templates:**
- `dns://crm.example.com/customers` — List all customers (auto-discovery)
- `dns://crm.example.com/customers/{customer_id}` — Specific customer data
- `dns://crm.example.com/customers/{customer_id}/invoices` — Customer's invoices
- `dns://crm.example.com/customers/{customer_id}/settings` — Customer preferences

**Model Interaction:**
```
1. Model calls search-customers
2. Gets customer ID from result
3. Model discovers resource template: dns://crm.example.com/customers/{customer_id}/invoices
4. Model reads invoices via resource (no additional tool call needed)
```

---

## Design Pattern 2: Hierarchical Resource Organization

Organize resources using DNS-like hierarchies to reflect the data model:

```
dns://api.example.com/
  /users/{id}/
    /settings
    /notifications
    /billing/
      /invoices/{invoice_id}
      /payments/{payment_id}
  /teams/{team_id}/
    /members
    /projects/{project_id}
      /issues/{issue_id}
```

**Benefits:**
- Predictable naming — models can guess resource URIs
- Clear relationships — `/users/{id}/settings` clearly belongs to a user
- Composable — tools and resources work together naturally
- Discoverable — templates enable progressive exploration

---

## Design Pattern 3: Bounded Responses with Pagination

Never return unbounded arrays. Always paginate and limit sizes.

### Pattern: Cursor-Based Pagination

```python
from pydantic import BaseModel, Field
from typing import List, Optional

class PaginatedResponse(BaseModel):
    """Paginated results with cursor for next batch."""
    items: List[dict] = Field(..., max_items=100)
    next_cursor: Optional[str] = Field(None, description="Cursor for next page")
    has_more: bool = Field(False, description="Whether more results exist")
```

### Tool Example: List Customers with Pagination

```python
from pydantic import BaseModel, Field
from typing import Optional

class ListCustomersInput(BaseModel):
    """List customers with pagination."""
    limit: int = Field(10, ge=1, le=100, description="Max results per page")
    cursor: Optional[str] = Field(None, description="Pagination cursor from previous response")
    filter_status: Optional[str] = Field(None, description="Filter by status: active, inactive, trial")

class ListCustomersOutput(BaseModel):
    """Paginated customer list."""
    customers: List[dict] = Field(..., description="Up to 'limit' customers")
    next_cursor: Optional[str] = Field(None, description="Pass to next call for more results")
    total_available: int = Field(..., description="Approximate total count")

async def list_customers(input: ListCustomersInput) -> ListCustomersOutput:
    """
    List customers with optional filtering and pagination.
    
    Always limits results to prevent response bloat.
    Use cursor for pagination, not offset (more efficient).
    """
    # Enforce max limit even if client requests more
    safe_limit = min(input.limit, 100)
    
    # Fetch one extra to detect if more results exist
    results = await db.query(
        "SELECT * FROM customers WHERE status = ?",
        input.filter_status or "active",
        limit=safe_limit + 1,
        offset_cursor=input.cursor
    )
    
    has_more = len(results) > safe_limit
    items = results[:safe_limit]
    
    next_cursor = None
    if has_more:
        next_cursor = items[-1]['id']  # Use last item's ID as cursor
    
    return ListCustomersOutput(
        customers=items,
        next_cursor=next_cursor,
        total_available=await db.count("SELECT COUNT(*) FROM customers")
    )
```

---

## Design Pattern 4: Strict Input Schemas with Constraints

Use Pydantic to enforce constraints at the boundary. Never accept open-ended dicts.

### ❌ BAD: Unbounded Input

```python
# ❌ NEVER DO THIS
class SearchInput(BaseModel):
    filters: dict  # Anything goes — model doesn't know constraints
    options: dict  # Unbounded — server must validate everything

# Problems:
# - Model can pass invalid filters
# - Server has to guess what's allowed
# - No IDE autocomplete or documentation
```

### ✅ GOOD: Constrained Input

```python
from pydantic import BaseModel, Field
from enum import Enum

class CustomerStatus(str, Enum):
    ACTIVE = "active"
    INACTIVE = "inactive"
    TRIAL = "trial"

class SearchCustomersInput(BaseModel):
    """Search and filter customers."""
    name: Optional[str] = Field(
        None, 
        min_length=2, 
        max_length=100,
        description="Customer name to search for (partial match OK)"
    )
    status: CustomerStatus = Field(
        CustomerStatus.ACTIVE,
        description="Filter by account status"
    )
    country_code: Optional[str] = Field(
        None,
        regex="^[A-Z]{2}$",
        description="2-letter ISO country code (US, UK, FR, etc)"
    )
    limit: int = Field(
        10,
        ge=1,
        le=100,
        description="Results per page (1-100)"
    )

async def search_customers(input: SearchCustomersInput) -> List[dict]:
    """
    Search customers with strict, validated filters.
    
    Benefits:
    - Model knows exactly what's allowed
    - Constraints enforced before DB call
    - Clear error messages if invalid
    """
    # All input is already validated by Pydantic
    # No defensive checks needed
    results = await db.search_customers(
        name=input.name,
        status=input.status,
        country=input.country_code,
        limit=input.limit
    )
    return results
```

---

## Design Pattern 5: Tool Annotations (Hints)

Use annotations to communicate tool behavior to the model:

### readOnlyHint

Indicates a tool doesn't mutate server state.

```python
from mcp.server.models import Tool

read_only_tool = Tool(
    name="fetch-customer-profile",
    description="Retrieve customer profile information",
    inputSchema={...},
    readOnlyHint=True  # ← Tells model this is safe to call multiple times
)
```

**When to use:**
- Query/search tools
- Read-only data fetches
- Status checks
- Analytics/reporting tools

**Effect:** Models can call these freely without worrying about side effects.

---

### destructiveHint

Indicates a tool makes irreversible changes.

```python
destroy_tool = Tool(
    name="delete-customer-account",
    description="Permanently delete a customer account and all associated data",
    inputSchema={...},
    destructiveHint=True  # ← Tells model this requires careful reasoning
)
```

**When to use:**
- Delete operations
- Account closures
- Data purges
- Billing cancellations

**Effect:** Models treat these with extra caution, may ask for confirmation.

---

### idempotentHint

Indicates a tool is safe to retry.

```python
idempotent_tool = Tool(
    name="create-invoice",
    description="Create an invoice. Safe to retry with same inputs.",
    inputSchema={...},
    idempotentHint=True  # ← Tells model retries are safe
)
```

**When to use:**
- Operations where duplicate calls produce the same result
- Tools that check for existing resources before creating
- Upsert operations (create or update)
- Idempotent state transitions

**Example Implementation:**

```python
async def create_invoice(input: CreateInvoiceInput) -> CreateInvoiceOutput:
    """
    Create or fetch an invoice.
    
    Idempotent: calling twice with same input returns same invoice_id.
    Model can safely retry on transient errors.
    """
    # Check if invoice already exists
    existing = await db.query(
        "SELECT id FROM invoices WHERE customer_id = ? AND reference_id = ?",
        input.customer_id,
        input.reference_id
    )
    
    if existing:
        return CreateInvoiceOutput(invoice_id=existing[0]['id'], created=False)
    
    # Create new invoice
    new_id = await db.insert("invoices", {...})
    return CreateInvoiceOutput(invoice_id=new_id, created=True)
```

---

### openWorldHint

Indicates a tool or resource supports unbounded discovery.

```python
# Use for resource templates that can explore many possibilities
list_resource = ResourceTemplate(
    uriTemplate="dns://api.example.com/items",
    description="List all items. Supports dynamic filtering.",
    mimeType="application/json",
    openWorldHint=True  # ← Model can explore unknown items dynamically
)
```

**When to use:**
- Resources that support arbitrary filtering
- APIs with unknown/dynamic data
- Exploration-heavy workflows

---

## Design Pattern 6: Stateless vs Stateful Tools

### Stateless Tool (Preferred)

Returns complete results without requiring previous context.

```python
# ✅ GOOD: Stateless, self-contained
class FetchInvoiceInput(BaseModel):
    invoice_id: str

class FetchInvoiceOutput(BaseModel):
    id: str
    customer_id: str
    total: float
    status: str
    items: List[dict]

async def fetch_invoice(input: FetchInvoiceInput) -> FetchInvoiceOutput:
    """Fetch full invoice details. Works regardless of call history."""
    return await db.fetch_invoice(input.invoice_id)
```

**Advantages:**
- Can be called in any order
- Result is always the same
- Easier for models to reason about
- Composable with other tools

---

### Stateful Tool (Use Rarely)

Maintains context from previous calls (conversational flow).

```python
# ⚠️ ONLY if necessary: Stateful, context-dependent
class UpdateInvoiceInput(BaseModel):
    invoice_id: str = None  # Optional if using context
    field: str  # Which field to update
    value: str  # New value

async def update_invoice(input: UpdateInvoiceInput, context: Dict) -> dict:
    """Update invoice. Depends on 'current_invoice' in context."""
    invoice_id = input.invoice_id or context.get('current_invoice')
    if not invoice_id:
        raise ValueError("No current invoice in context")
    
    # Update...
```

**When Stateful is OK:**
- Multi-step workflows with required sequence
- Tools that operate on "current selection"
- Interactive UIs or terminal-like interfaces

**Better Alternative:** Use tool parameters instead of context.

---

## Anti-Patterns & Fixes

### Anti-Pattern 1: Tool Bloat (>15 Tools)

**Problem:** Having 20+ tools in a single server

```python
# ❌ BAD: Too many tools
tools = [
    "get_user",
    "get_users",
    "search_users",
    "create_user",
    "update_user",
    "update_user_profile",
    "update_user_settings",
    "delete_user",
    "ban_user",
    "get_user_invoices",
    "get_user_payments",
    # ... 10 more ...
]
# Model gets confused about which tool to use
# Token usage explodes with tool descriptions
# Response time suffers
```

**Solution:** Use Progressive Discovery

```python
# ✅ GOOD: Core tools only (5–10)
tools = [
    "search_users",        # Find users
    "fetch_user_details",  # Get full profile
    "create_user",         # Create new user
    "update_user",         # Update user (generic)
]

# Add depth via resources
resources = [
    "dns://api.example.com/users/{id}",              # User details
    "dns://api.example.com/users/{id}/invoices",     # Invoices
    "dns://api.example.com/users/{id}/payments",     # Payments
    "dns://api.example.com/users/{id}/settings",     # Settings
]

# Model can discover and navigate the hierarchy
```

**Impact:**
- Better model reasoning (fewer options)
- Faster responses (less to evaluate)
- Clearer user intent

---

### Anti-Pattern 2: Vague Descriptions

**Problem:** Unclear tool purposes

```python
# ❌ BAD: Ambiguous descriptions
Tool(
    name="get_data",
    description="Get data from the system"  # What data? Which system?
)

Tool(
    name="process",
    description="Process something"  # Process what? How?
)

Tool(
    name="search",
    description="Search"  # Search where? What fields?
)
```

**Solution:** Specific, Actionable Descriptions

```python
# ✅ GOOD: Clear intent and usage
Tool(
    name="fetch_customer_invoices",
    description="Retrieve all invoices for a specific customer, optionally filtered by date range and status"
)

Tool(
    name="create_invoice_from_order",
    description="Generate an invoice from an existing order. Links the invoice to the order and sends payment notification"
)

Tool(
    name="search_customers_by_email",
    description="Search customer database by email address. Returns up to 10 matching customers"
)
```

**Impact:**
- Model knows exactly what each tool does
- Fewer wrong tool selections
- Better first-try success rate

---

### Anti-Pattern 3: Unbounded Response Arrays

**Problem:** Returning unlimited results

```python
# ❌ BAD: No size limit
class SearchInput(BaseModel):
    query: str

class SearchOutput(BaseModel):
    results: List[dict]  # Could be millions of items!

async def search(input: SearchInput) -> SearchOutput:
    """Search without pagination — response could be massive."""
    all_results = await db.query("SELECT * WHERE ...")
    return SearchOutput(results=all_results)  # Disaster!
```

**Solution:** Always Paginate

```python
# ✅ GOOD: Bounded, paginated responses
class SearchInput(BaseModel):
    query: str
    limit: int = Field(50, ge=1, le=100)
    cursor: Optional[str] = None

class SearchOutput(BaseModel):
    results: List[dict] = Field(..., max_items=100)
    next_cursor: Optional[str]
    total_available: int

async def search(input: SearchInput) -> SearchOutput:
    """Search with pagination — bounded response size."""
    safe_limit = min(input.limit, 100)
    results = await db.query(
        "SELECT * WHERE ... LIMIT ?",
        safe_limit + 1,
        cursor=input.cursor
    )
    has_more = len(results) > safe_limit
    return SearchOutput(
        results=results[:safe_limit],
        next_cursor=results[-1]['id'] if has_more else None,
        total_available=total_count
    )
```

**Impact:**
- Predictable response sizes
- No memory exhaustion
- Better model performance

---

### Anti-Pattern 4: Too Many Resource Templates

**Problem:** Explosion of discovery options

```python
# ❌ BAD: 30+ resource templates → discovery chaos
templates = [
    "dns://api.example.com/users",
    "dns://api.example.com/users/{id}",
    "dns://api.example.com/users/{id}/profile",
    "dns://api.example.com/users/{id}/profile/name",  # Too granular
    "dns://api.example.com/users/{id}/profile/email",
    "dns://api.example.com/users/{id}/profile/phone",
    # ... many more micro-templates ...
]
# Model gets lost trying to navigate
```

**Solution:** Organize by Natural Hierarchy

```python
# ✅ GOOD: Grouped, hierarchical templates
templates = [
    "dns://api.example.com/users",                      # List all
    "dns://api.example.com/users/{id}",                 # User details
    "dns://api.example.com/users/{id}/invoices",        # Related data
    "dns://api.example.com/users/{id}/settings",        # Configuration
]

# Client fetches /users/{id} which includes profile info
# No need for micro-templates
```

**Impact:**
- Clear navigation paths
- Fewer discovery dead-ends
- Faster model reasoning

---

## Constraints

### MUST DO

- **Write specific, actionable descriptions** — "Fetch customer invoice by invoice_id" not "Get data"
- **Use Pydantic models for all inputs/outputs** — Enable validation and IDE support
- **Paginate all list responses** — Never return unbounded arrays
- **Organize resources hierarchically** — Parent/child relationships clear from URI
- **Annotate tools appropriately** — readOnlyHint, destructiveHint, idempotentHint where applicable
- **Start with 5–10 core tools** — Add depth via resources, not more tools
- **Constrain all numeric/string fields** — min/max lengths, value ranges, regex patterns
- **Test tool behavior under edge cases** — Empty lists, null values, rate limiting

### MUST NOT DO

- Create tools with open-ended dict inputs/outputs
- Return unbounded arrays or lists
- Mix multiple responsibilities in one tool
- Use vague names like `get_data` or `process`
- Create tools that depend on hidden context
- Add tools without clear use cases (avoid speculative tools)
- Skip descriptions or use placeholder text
- Design tools that require specific call ordering (stateful sequences)
- Return raw database objects without transformation
- Ignore error cases — always handle and return descriptive errors

---

## Output Template

When designing MCP tools and resources, your output must include:

1. **Tool Inventory** — List of 5–10 core tools with names and brief descriptions
2. **Resource Hierarchy** — ASCII diagram showing parent/child relationships
3. **Tool Schema Examples** — 2–3 complete Pydantic schemas (input + output)
4. **Annotation Strategy** — Which tools get readOnlyHint, destructiveHint, idempotentHint
5. **Progressive Discovery Flow** — How models discover and use resources after tools
6. **Bounded Responses Plan** — Pagination strategy for list endpoints
7. **Anti-Pattern Review** — Checklist of patterns avoided (no tool bloat, vague descriptions, etc.)

---

## Related Skills

| Skill | Purpose |
|---|---|
| `mcp-server-fastmcp-python` | Implement MCP server with FastMCP framework |
| `mcp-client-integration` | Integrate MCP clients to call tools and read resources |

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

Email Composer

Draft professional emails for various contexts including business, technical, and customer communication. Use when the user needs help writing emails or composing professional messages.

304952 votes

Solution Architect

Designs system architecture, component specifications, and technical integration strategy. Use when: designing solutions, system architecture, technology stack, or integration approaches.

192 votes

Akorchak:Venture Assessment

Generate a comprehensive VC investment assessment report for a company

72 votes

Telegram Compose

Compose rich, readable Telegram messages using HTML formatting via direct Telegram API. Use when: (1) Sending any Telegram message beyond a simple one-line reply, (2) Creating structured messages with sections, lists, or status updates, (3) Need formatting unavailable via Clawdbot's Markdown conversion (underline, spoilers, expandable blockquotes, user mentions by ID), (4) Sending alerts, reports, summaries, or notifications to Telegram, (5) Want professional, scannable message formatting wit...

6511 votes

Just Fucking Cancel

Find and cancel unwanted subscriptions by analyzing bank transactions. Detects recurring charges, calculates annual waste, and helps you cancel with direct URLs and browser automation. Use when: 'cancel subscriptions', 'audit subscriptions', 'find recurring charges', 'what am I paying for', 'save money', 'subscription cleanup', 'stop wasting money'. Supports CSV import (Apple Card, Chase, Amex, Citi, Bank of America, Capital One, Mint, Copilot) OR Plaid API for automatic transaction pull. Out...

6511 votes
View all in business →