Skip to main content

mcp-tool-design-patterns

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.

الانتقال إلى التثبيت

معلومات المصدر

المستودع
paulpas/agent-skill-router
آخر نشاط في المصدر
٢٣ سبتمبر ٢٠٢٦ في ٢١:٥٩
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٦
التفرعات
٠

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

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
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub