> **Archived fleet import** — From **notepadpp-mcp** / generic fleet dev notes. **Not** pywinauto-mcp source of truth. See [DEVELOPMENT.md](../../DEVELOPMENT.md) and [TESTING.md](../../TESTING.md).
# MCP Prompt Templates vs Docstrings
## Managing Duplication in MCPB Packages

**Problem**: When you package an MCP server with `mcpb pack`, you can include prompt templates that duplicate your docstrings, creating maintenance burden.

**Solution**: Strategic approach to minimize duplication while maximizing Claude's success rate.

---

## The Duplication Challenge

### What You Have to Maintain

1. **Python Docstring** (in your code)
   ```python
   def search_bookmarks(query: str, profile_path: Optional[str] = None):
       """Search Firefox bookmarks by title/URL (requires Firefox closed).
       
       Args:
           query (str, REQUIRED): Search term...
           profile_path (str, OPTIONAL): Full path...
       ...
       """
   ```

2. **Prompt Template** (in mcpb package)
   ```
   # prompts/search_bookmarks.md
   When using the search_bookmarks tool:
   - Always close Firefox first
   - Use get_firefox_profiles() to find the path
   - Examples: search_bookmarks(query="plex", limit=20)
   ```

**Duplication Risk**: Change docstring but forget prompt template = inconsistent behavior

---

## Three Approaches to This Problem

### Approach 1: Docstring Only (Minimal Duplication)

**When to use**: Simple tools, low token budget, minimal confusion risk

**Strategy**:
- Write comprehensive docstrings (following the standard)
- No separate prompt template
- MCP framework shows docstring to Claude

**Pros**:
- ✅ Single source of truth
- ✅ No sync issues
- ✅ Less maintenance

**Cons**:
- ❌ Docstrings optimized for humans, not LLMs
- ❌ May include dev-specific details Claude doesn't need
- ❌ Can't optimize token usage

**Example Package Structure**:
```
my-mcp-server/
├── src/
│   └── tools.py  # Comprehensive docstrings
├── mcp.json      # No prompt templates
└── README.md
```

---

### Approach 2: Prompt Template + Minimal Docstring (LLM-First)

**When to use**: Complex tools, high token budget, Claude is primary consumer

**Strategy**:
- Minimal docstring (for IDE/developers)
- Comprehensive prompt template (for Claude)
- Prompt template is the primary documentation

**Pros**:
- ✅ Optimized for LLM consumption
- ✅ Can be more conversational
- ✅ Token-optimized
- ✅ Can include LLM-specific guidance

**Cons**:
- ❌ Developers get less help in IDE
- ❌ Prompt template not visible in code
- ❌ Still duplication (but reversed priority)

**Example Package Structure**:
```
my-mcp-server/
├── src/
│   └── tools.py        # Minimal docstrings
├── prompts/
│   ├── search_bookmarks.md
│   └── list_bookmarks.md
├── mcp.json            # References prompts/
└── README.md
```

---

### Approach 3: Hybrid with Generation (Best of Both Worlds)

**When to use**: Professional MCP servers, automation-friendly workflow

**Strategy**:
- Write ONE comprehensive source (markdown or JSON)
- Auto-generate BOTH docstring and prompt template
- Build script keeps them in sync

**Pros**:
- ✅ Single source of truth
- ✅ Optimized for both humans and LLMs
- ✅ No sync issues
- ✅ Can optimize each output format

**Cons**:
- ❌ Requires build tooling
- ❌ More complex workflow
- ❌ Initial setup overhead

**Example Package Structure**:
```
my-mcp-server/
├── docs/
│   └── tools.yaml           # Single source of truth
├── src/
│   └── tools.py             # GENERATED docstrings
├── prompts/
│   └── search_bookmarks.md  # GENERATED from docs/
├── scripts/
│   └── generate_docs.py     # Generation script
├── mcp.json
└── README.md
```

---

## Recommended: Approach 3 with YAML Source

### Single Source of Truth Format

```yaml
# docs/tools/search_bookmarks.yaml
name: search_bookmarks
category: firefox
summary: Search Firefox bookmarks by title/URL (requires Firefox closed)

prerequisites:
  - Firefox must be completely closed
  - Profile must exist with places.sqlite
  - Read permissions on profile directory

parameters:
  - name: query
    type: string
    required: true
    description: Search term to find in titles/URLs
    format: Case-insensitive partial match
    examples:
      - "plex"
      - "github.com"
      - "machine learning"
    
  - name: profile_path
    type: string
    required: false
    default: Auto-detect from profiles.ini
    description: Full path to Firefox profile directory
    format: "C:\\Users\\{user}\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\{profile}"
    examples:
      - "C:\\Users\\sandr\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\airiswdq.default-release"
    get_value_from: get_firefox_profiles()
    
  - name: limit
    type: integer
    required: false
    default: 50
    range: [1, 100]
    description: Maximum results to return

returns:
  success:
    status: success
    query: str
    count: int
    results:
      - id: int
        title: str
        url: str
        dateAdded: int
        parent: int
  error:
    status: error
    error: str
    suggestion: str

errors:
  - condition: "Database is locked"
    cause: Firefox is running
    fix: Close Firefox completely (check Task Manager)
    workaround: Use export_bookmarks() instead
    
  - condition: "Profile not found"
    cause: Invalid path or doesn't exist
    fix: Use get_firefox_profiles() to find path
    check: Verify places.sqlite exists

examples:
  - name: Basic search (most common)
    when: Single profile, Firefox closed
    code: |
      search_bookmarks(query="plex")
    returns: Up to 50 bookmarks matching "plex"
    
  - name: Specific profile
    when: Multiple profiles or auto-detect fails
    code: |
      search_bookmarks(
          query="immich",
          profile_path="C:\\Users\\sandr\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\airiswdq.default-release",
          limit=20
      )
    returns: Up to 20 bookmarks from specified profile

platforms:
  windows:
    location: "%APPDATA%\\Mozilla\\Firefox\\Profiles\\"
    format: Double backslashes "C:\\Users\\..."
    check_process: Task Manager → firefox.exe
    
  linux:
    location: "~/.mozilla/firefox/"
    format: Forward slashes "/home/user/..."
    check_process: ps aux | grep firefox
    
  macos:
    location: "~/Library/Application Support/Firefox/Profiles/"
    format: Forward slashes (with spaces)
    check_process: Activity Monitor

see_also:
  - get_firefox_profiles: Find available profiles (use first)
  - list_bookmarks: Browse all bookmarks
  - export_bookmarks: Export while Firefox open
```

---

### Generation Script

```python
# scripts/generate_docs.py
import yaml
from pathlib import Path
from jinja2 import Template

def generate_docstring(tool_spec):
    """Generate Python docstring from YAML spec"""
    template = Template('''"""{{ summary }}
    
    {{ description }}
    
    Prerequisites:
    {% for prereq in prerequisites %}    - {{ prereq }}
    {% endfor %}
    
    Args:
    {% for param in parameters %}    {{ param.name }} ({{ param.type }}, {{ 'REQUIRED' if param.required else 'OPTIONAL' }}):
            {{ param.description }}
            {% if param.format %}Format: {{ param.format }}{% endif %}
            {% if param.examples %}Examples: {{ param.examples|join(', ') }}{% endif %}
            {% if not param.required %}Default: {{ param.default }}{% endif %}
            {% if param.get_value_from %}Get value: {{ param.get_value_from }}{% endif %}
            
    {% endfor %}
    Returns:
        dict: {{ returns.success.status }}
        
        Structure (Success):
        {{ returns.success|format_structure }}
        
        Structure (Error):
        {{ returns.error|format_structure }}
    
    Common Issues:
    {% for error in errors %}    {{ loop.index }}. "{{ error.condition }}"
           CAUSE: {{ error.cause }}
           FIX: {{ error.fix }}
           {% if error.workaround %}WORKAROUND: {{ error.workaround }}{% endif %}
           
    {% endfor %}
    Examples:
    {% for example in examples %}    # {{ example.name }}
        # Use when: {{ example.when }}
        {{ example.code }}
        # Returns: {{ example.returns }}
        
    {% endfor %}
    Platform Notes:
    {% for platform, details in platforms.items %}    {{ platform.upper() }}:
            - Location: {{ details.location }}
            - Format: {{ details.format }}
            - Check: {{ details.check_process }}
            
    {% endfor %}
    See Also:
    {% for tool, desc in see_also.items %}    - {{ tool }}(): {{ desc }}
    {% endfor %}"""''')
    
    return template.render(**tool_spec)

def generate_prompt_template(tool_spec):
    """Generate Claude prompt template from YAML spec"""
    template = Template('''# {{ name }} Tool Guide

## Quick Summary
{{ summary }}

**CRITICAL**: {% for prereq in prerequisites %}{{ prereq }}{% if not loop.last %}, {% endif %}{% endfor %}

## How to Use

### Parameters
{% for param in parameters %}
**{{ param.name }}** ({{ 'REQUIRED' if param.required else 'OPTIONAL' }})
- What: {{ param.description }}
- {% if param.examples %}Example: `{{ param.examples[0] }}`{% endif %}
- {% if not param.required %}Default: {{ param.default }}{% endif %}
{% if param.get_value_from %}- Get from: {{ param.get_value_from }}{% endif %}

{% endfor %}

### Common Patterns

{% for example in examples %}
**{{ example.name }}**
```python
{{ example.code }}
```
Use when: {{ example.when }}

{% endfor %}

### When Things Go Wrong

{% for error in errors %}
**Problem**: {{ error.condition }}
- Why: {{ error.cause }}
- Fix: {{ error.fix }}
{% if error.workaround %}- Alternative: {{ error.workaround }}{% endif %}

{% endfor %}

### Platform Differences

{% for platform, details in platforms.items %}
**{{ platform.title() }}**: {{ details.location }}
- Check if closed: {{ details.check_process }}
{% endfor %}

### Related Tools
{% for tool, desc in see_also.items %}
- `{{ tool }}()` - {{ desc }}
{% endfor %}
''')
    
    return template.render(**tool_spec)

def generate_all():
    """Generate all documentation from YAML sources"""
    docs_dir = Path("docs/tools")
    
    for yaml_file in docs_dir.glob("*.yaml"):
        with open(yaml_file) as f:
            spec = yaml.safe_load(f)
        
        # Generate docstring (for insertion into Python code)
        docstring = generate_docstring(spec)
        output_file = Path("generated/docstrings") / f"{spec['name']}.txt"
        output_file.parent.mkdir(exist_ok=True)
        output_file.write_text(docstring)
        
        # Generate prompt template (for mcpb package)
        prompt = generate_prompt_template(spec)
        prompt_file = Path("prompts") / f"{spec['name']}.md"
        prompt_file.parent.mkdir(exist_ok=True)
        prompt_file.write_text(prompt)
        
        print(f"✅ Generated docs for {spec['name']}")

if __name__ == "__main__":
    generate_all()
```

---

### Workflow Integration

```toml
# pyproject.toml
[tool.mcp]
generate_docs = "scripts/generate_docs.py"

[tool.mcp.build]
pre_build = ["python scripts/generate_docs.py"]
```

```bash
# Build workflow
python scripts/generate_docs.py  # Generate docstrings + prompt templates
mcpb validate                     # Validate package
mcpb pack                        # Package with generated prompts
```

---

## Prompt Template Best Practices

### What to Include in Prompt Templates (That Docstrings Don't Need)

1. **Conversational Tone**
   ```markdown
   # search_bookmarks Tool
   
   This tool helps you find bookmarks in Firefox. Before using it,
   **make sure Firefox is completely closed** - otherwise the database
   will be locked and the search will fail.
   ```

2. **Common User Mistakes**
   ```markdown
   ## Common Mistakes to Avoid
   
   ❌ Don't use this while Firefox is running
   ❌ Don't use relative paths for profile_path
   ❌ Don't forget to call get_firefox_profiles() first
   
   ✅ Close Firefox completely
   ✅ Use full absolute paths
   ✅ Get profile path from get_firefox_profiles()
   ```

3. **Decision Trees**
   ```markdown
   ## When to Use This Tool
   
   Use search_bookmarks() when:
   - You know what you're looking for (have search term)
   - Firefox is closed
   - You want fast, targeted results
   
   Use list_bookmarks() instead when:
   - You want to browse all bookmarks
   - You don't know what you're searching for
   - You want folder structure
   ```

4. **LLM-Specific Guidance**
   ```markdown
   ## For AI Assistants
   
   Before calling this tool:
   1. Check if Firefox is mentioned as running in conversation
   2. If user has multiple profiles, call get_firefox_profiles() first
   3. If this fails with "locked", suggest export_bookmarks() alternative
   
   After calling:
   - If count is 0, suggest broader search terms
   - If count is high, offer to filter results
   - Always show at least title and URL for each result
   ```

---

## Token Optimization

### Docstring vs Prompt Template Token Usage

**Full Docstring** (comprehensive): ~500-1000 tokens per tool
**Prompt Template** (optimized): ~200-400 tokens per tool

### Optimization Strategies

1. **Use References Instead of Repetition**
   ```markdown
   # Instead of repeating full structure:
   Returns the same structure as list_bookmarks() but filtered by query.
   See list_bookmarks documentation for full return structure.
   ```

2. **Common Errors Document**
   ```markdown
   # Create shared errors.md
   All Firefox tools may encounter these errors: [see errors.md]
   
   Tool-specific errors:
   - "No results found" → Try broader search terms
   ```

3. **Platform Notes Once**
   ```markdown
   # Share platform.md across all Firefox tools
   Firefox Profile Locations: [see platform.md]
   ```

4. **Conditional Inclusion**
   ```json
   // mcp.json
   {
     "prompts": {
       "search_bookmarks": {
         "file": "prompts/search_bookmarks.md",
         "include_shared": ["firefox_common", "platform_notes"]
       }
     }
   }
   ```

---

## Recommended Structure for MCP Packages

```
my-firefox-mcp/
├── src/
│   └── tools.py                # Minimal docstrings
├── docs/
│   ├── tools/
│   │   ├── search_bookmarks.yaml
│   │   ├── list_bookmarks.yaml
│   │   └── get_firefox_profiles.yaml
│   └── shared/
│       ├── firefox_common.md   # Shared concepts
│       ├── platform.md         # Platform differences
│       └── errors.md           # Common errors
├── prompts/                    # GENERATED
│   ├── search_bookmarks.md
│   └── list_bookmarks.md
├── generated/                  # GENERATED
│   └── docstrings/
│       └── search_bookmarks.txt
├── scripts/
│   └── generate_docs.py
├── mcp.json
└── README.md
```

---

## Practical Example: Your dbops Server

### Current State (Duplication Hell)
```python
# In Python code
def search_bookmarks(query, profile_path=None, limit=50):
    """Search bookmarks by title or URL."""  # ❌ Inadequate
```

```markdown
# In prompts/search_bookmarks.md (if it existed)
When searching Firefox bookmarks:
- Close Firefox first
- Use get_firefox_profiles() to find path
- Example: search_bookmarks(query="plex")
```

**Problem**: Two places to update, easily get out of sync

---

### Recommended Approach

```yaml
# docs/tools/search_bookmarks.yaml (SINGLE SOURCE)
name: search_bookmarks
category: firefox
summary: Search Firefox bookmarks (requires Firefox closed)
# ... full spec as shown above
```

```python
# scripts/generate_docs.py
# Generates both:
# - src/tools.py docstring
# - prompts/search_bookmarks.md
```

```bash
# Build command
python scripts/generate_docs.py && mcpb pack
```

**Benefit**: Change YAML once, both outputs updated automatically

---

## Migration Path

### Step 1: Audit Current State (5 min)
```bash
# Count your duplication
grep -r "def.*(" src/ | wc -l        # Python functions
find prompts/ -name "*.md" | wc -l   # Prompt templates
# If prompt_count > 0: You have duplication
```

### Step 2: Choose Approach (5 min)
- Simple server (<10 tools): Approach 1 (docstring only)
- Medium server (10-30 tools): Approach 2 (prompt template primary)
- Complex server (>30 tools): Approach 3 (generated)

### Step 3: Implement (varies)
- Approach 1: Enhance docstrings, remove prompt templates
- Approach 2: Write comprehensive prompt templates, minimal docstrings
- Approach 3: Create YAML specs, write generation script

### Step 4: Automate (1 hour)
```python
# Add pre-commit hook
# .git/hooks/pre-commit
#!/bin/bash
python scripts/generate_docs.py
git add generated/ prompts/
```

---

## Conclusion

**The Duplication Dilemma**: Docstrings vs prompt templates

**The Solution**: 
- Small servers: Comprehensive docstrings only
- Large servers: Single source YAML → generate both
- All servers: Never manually maintain both!

**The Tools**:
- YAML for canonical source
- Jinja2 for generation
- Pre-commit hooks for automation
- mcpb for packaging

**The Result**: Claude succeeds on first try, developers get great IDE support, no sync issues.

---

## References

- **MCPB Documentation**: https://github.com/anthropics/mcpb
- **MCP Prompt Templates**: [Link to official docs]
- **FastMCP Guide**: https://github.com/jlowin/fastmcp
- **This Document**: Part of MCP Tool Documentation Standards v1.0
