# Directory Structure
```
├── .gitignore
├── .python-version
├── main.py
├── pyproject.toml
├── README.md
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
```
3.13
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Bear App MCP Server
A Model Context Protocol (MCP) server that provides Claude with access to your Bear App notes. Search, retrieve, and analyze your Bear notes directly from Claude Desktop or any MCP-compatible client.
## Features
### 🔍 Search & Discovery
- **Full-text search** across all your notes
- **Tag-based filtering** for organized content
- **Title-based search** with exact or partial matching
- **Recent notes** filtering by modification date
### 💻 Code & Technical Content
- **Kubernetes manifest finder** - Locate deployment examples, service configs, etc.
- **Code example search** - Find code snippets by programming language
- **Code block extraction** - Automatically parse and categorize code blocks
- **Technical documentation** discovery
### 📊 Content Analysis
- **Word count** and content statistics
- **Code language detection** from fenced code blocks
- **Content previews** for quick scanning
- **Metadata extraction** (creation/modification dates)
## Installation
### Prerequisites
- macOS (Bear App is macOS/iOS only)
- Bear App installed and with some notes
- Python 3.8 or higher
- Claude Desktop (for desktop integration)
### Setup Steps
1. **Clone or download the server script**
   ```bash
   git clone github.com/netologist/mcp-bear-notes
   ```
2. **Create a virtual environment**
   ```bash
   uv install
   source .venv/bin/activate
   ```
4. **Test the server**
   ```bash
   uv run python main.py
   ```
## Claude Desktop Integration
### Configuration File Location
Edit your Claude Desktop configuration file:
```
~/Library/Application Support/Claude/claude_desktop_config.json
```
### Add MCP Server Configuration
```json
{
  "mcpServers": {
    "bear-notes": {
      "command": "/full/path/to/mcp-bear-notes/.venv/bin/python",
      "args": ["/full/path/to/mcp-bear-notes/main.py"],
      "env": {
        "PYTHONPATH": "/full/path/to/mcp-bear-notes/.venv/lib/python3.13/site-packages"
      }
    }
  }
}
```
**Important**: Replace `/full/path/to/` with your actual file paths.
### Restart Claude Desktop
Close and reopen Claude Desktop to load the MCP server.
## Usage Examples
Once integrated with Claude Desktop, you can use natural language to interact with your Bear notes:
### Basic Search
```
"Search my Bear notes for Docker examples"
"Find notes about Python APIs"
"Show me my recent notes from this week"
```
### Technical Content
```
"Find my Kubernetes deployment manifests"
"Look for JavaScript code examples in my notes"
"Show me notes with YAML configurations"
```
### Specific Retrieval
```
"Get the note titled 'Development Setup'"
"Find notes tagged with 'work'"
"Show me all my available tags"
```
## Available Tools
### `search_bear_notes(query, tag, limit)`
Search notes by content and tags.
- `query`: Text to search for
- `tag`: Filter by specific tag (without #)
- `limit`: Max results (default: 20)
### `get_bear_note(note_id)`
Retrieve a specific note by its unique ID.
- `note_id`: Bear note's unique identifier
### `find_kubernetes_examples(resource_type)`
Find Kubernetes-related content.
- `resource_type`: K8s resource (deployment, service, etc.)
### `find_code_examples(language, topic, limit)`
Search for code examples.
- `language`: Programming language
- `topic`: Code topic/domain
- `limit`: Max results (default: 15)
### `find_notes_by_title(title_query, exact_match)`
Search notes by title.
- `title_query`: Title text to search
- `exact_match`: Exact or partial matching
### `get_recent_notes(days, limit)`
Get recently modified notes.
- `days`: How many days to look back (default: 7)
- `limit`: Max results (default: 20)
### `list_bear_tags()`
List all tags found in your notes.
## Bear Database Information
The server reads from Bear's SQLite database located at:
```
~/Library/Group Containers/9K33E3U3T4.net.shinyfrog.bear/Application Data/database.sqlite
```
### Database Access Notes
- **Read-only access** - The server never modifies your notes
- **No authentication required** - Uses direct SQLite access
- **Performance** - Database queries are optimized for speed
- **Safety** - Only accesses non-trashed notes
## Troubleshooting
### Common Issues
**Server won't start**
- Check Python path in configuration
- Verify virtual environment activation
- Ensure fastmcp is installed
**No notes found**
- Verify Bear database path exists
- Check that Bear App has been opened at least once
- Confirm notes aren't in trash
**Claude Desktop integration fails**
- Validate JSON syntax in config file
- Check file paths are absolute, not relative
- Restart Claude Desktop after configuration changes
**Permission denied errors**
- Ensure script has execute permissions: `chmod +x main.py`
- Check Bear database file permissions
### Debug Mode
Run the server directly to see debug output:
```bash
python main.py
```
### Log Files
Check Claude Desktop logs for MCP server errors:
```bash
~/Library/Logs/Claude/
```
## Security & Privacy
- **Local-only**: All data stays on your machine
- **Read-only**: Server never modifies your notes
- **No network**: No external connections required
- **Open source**: Full transparency of operations
## Contributing
Contributions welcome! Areas for improvement:
- Additional search filters
- Export functionality
- Note creation capabilities
- iOS Shortcuts integration
- Performance optimizations
## License
MIT License - See LICENSE file for details.
## Changelog
### v1.0.0
- Initial release
- Basic search and retrieval functions
- Kubernetes and code example finders
- Claude Desktop integration
- Tag listing and filtering
## Support
For issues and questions:
1. Check the troubleshooting section
2. Review Claude Desktop MCP documentation
3. Open an issue on GitHub
4. Check Bear App forums for database-related questions
---
**Note**: This is an unofficial tool and is not affiliated with Bear App or Anthropic. Use at your own discretion.
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
[project]
name = "mcp-bear-notes"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
    "fastmcp>=2.8.1",
]
```
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/env python3
"""
Bear App MCP Server
MCP server providing access to Bear App notes using FastMCP
"""
import sqlite3
import os
from pathlib import Path
from typing import List, Dict, Any, Optional
from fastmcp import FastMCP
# Bear App database path (macOS)
BEAR_DB_PATH = os.path.expanduser("~/Library/Group Containers/9K33E3U3T4.net.shinyfrog.bear/Application Data/database.sqlite")
# Initialize MCP server
mcp = FastMCP("Bear Notes")
def get_bear_db_connection():
    """Connect to Bear database"""
    if not os.path.exists(BEAR_DB_PATH):
        raise FileNotFoundError(f"Bear database not found: {BEAR_DB_PATH}")
    
    conn = sqlite3.connect(BEAR_DB_PATH)
    conn.row_factory = sqlite3.Row  # Enable column name access
    return conn
def search_notes(query: str = "", tag: str = "", limit: int = 20) -> List[Dict[str, Any]]:
    """Search Bear notes"""
    conn = get_bear_db_connection()
    
    try:
        # Base query
        sql = """
        SELECT 
            ZUNIQUEIDENTIFIER as id,
            ZTITLE as title,
            ZTEXT as content,
            ZCREATIONDATE as created_date,
            ZMODIFICATIONDATE as modified_date,
            ZTRASHED as is_trashed
        FROM ZSFNOTE 
        WHERE ZTRASHED = 0
        """
        
        params = []
        
        # Add search criteria
        if query:
            sql += " AND (ZTITLE LIKE ? OR ZTEXT LIKE ?)"
            params.extend([f"%{query}%", f"%{query}%"])
        
        # Add tag filter
        if tag:
            sql += " AND ZTEXT LIKE ?"
            params.append(f"%#{tag}%")
        
        sql += " ORDER BY ZMODIFICATIONDATE DESC LIMIT ?"
        params.append(limit)
        
        cursor = conn.execute(sql, params)
        results = []
        
        for row in cursor.fetchall():
            content = row["content"] or ""
            results.append({
                "id": row["id"],
                "title": row["title"] or "Untitled",
                "content": content,
                "created_date": row["created_date"],
                "modified_date": row["modified_date"],
                "preview": content[:200] + "..." if len(content) > 200 else content,
                "word_count": len(content.split()) if content else 0
            })
        
        return results
        
    finally:
        conn.close()
def get_note_by_id(note_id: str) -> Optional[Dict[str, Any]]:
    """Get a specific note by ID"""
    conn = get_bear_db_connection()
    
    try:
        cursor = conn.execute("""
            SELECT 
                ZUNIQUEIDENTIFIER as id,
                ZTITLE as title,
                ZTEXT as content,
                ZCREATIONDATE as created_date,
                ZMODIFICATIONDATE as modified_date
            FROM ZSFNOTE 
            WHERE ZUNIQUEIDENTIFIER = ? AND ZTRASHED = 0
        """, (note_id,))
        
        row = cursor.fetchone()
        if row:
            content = row["content"] or ""
            return {
                "id": row["id"],
                "title": row["title"] or "Untitled",
                "content": content,
                "created_date": row["created_date"],
                "modified_date": row["modified_date"],
                "word_count": len(content.split()) if content else 0
            }
        return None
        
    finally:
        conn.close()
def get_tags() -> List[str]:
    """List all tags from notes"""
    conn = get_bear_db_connection()
    
    try:
        cursor = conn.execute("""
            SELECT ZTEXT 
            FROM ZSFNOTE 
            WHERE ZTRASHED = 0 AND ZTEXT IS NOT NULL
        """)
        
        tags = set()
        for row in cursor.fetchall():
            content = row[0] or ""
            # Simple tag extraction (#tag format)
            import re
            found_tags = re.findall(r'#(\w+)', content)
            tags.update(found_tags)
        
        return sorted(list(tags))
        
    finally:
        conn.close()
def extract_code_blocks(content: str) -> List[Dict[str, str]]:
    """Extract code blocks from note content"""
    import re
    
    # Find code blocks with language specification
    code_blocks = []
    pattern = r'```(\w+)?\n(.*?)```'
    matches = re.findall(pattern, content, re.DOTALL)
    
    for language, code in matches:
        code_blocks.append({
            "language": language or "text",
            "code": code.strip()
        })
    
    return code_blocks
@mcp.tool()
def search_bear_notes(query: str = "", tag: str = "", limit: int = 20) -> List[Dict[str, Any]]:
    """
    Search Bear App notes
    
    Args:
        query: Text to search for (searches in title and content)
        tag: Tag to filter by (without # symbol)
        limit: Maximum number of results
    
    Returns:
        List of matching notes with metadata
    """
    try:
        return search_notes(query, tag, limit)
    except Exception as e:
        return [{"error": f"Search error: {str(e)}"}]
@mcp.tool()
def get_bear_note(note_id: str) -> Dict[str, Any]:
    """
    Get a specific Bear note by ID
    
    Args:
        note_id: Bear note's unique identifier
    
    Returns:
        Complete note content with metadata
    """
    try:
        note = get_note_by_id(note_id)
        if note:
            return note
        else:
            return {"error": "Note not found"}
    except Exception as e:
        return {"error": f"Error retrieving note: {str(e)}"}
@mcp.tool()
def list_bear_tags() -> List[str]:
    """
    List all tags from Bear App notes
    
    Returns:
        Sorted list of all tags found in notes
    """
    try:
        return get_tags()
    except Exception as e:
        return [f"Error listing tags: {str(e)}"]
@mcp.tool()
def find_kubernetes_examples(resource_type: str = "deployment") -> List[Dict[str, Any]]:
    """
    Find Kubernetes manifest examples in Bear notes
    
    Args:
        resource_type: Kubernetes resource type to search for (deployment, service, configmap, etc.)
    
    Returns:
        Notes containing Kubernetes examples
    """
    try:
        # Search for Kubernetes-related terms
        k8s_terms = [
            f"kind: {resource_type.title()}",
            f"apiVersion:",
            f"kubernetes {resource_type}",
            f"k8s {resource_type}",
            f"kubectl",
            "yaml",
            "manifest"
        ]
        
        results = []
        seen_ids = set()
        
        for term in k8s_terms:
            notes = search_notes(term, limit=10)
            for note in notes:
                if note["id"] not in seen_ids:
                    # Extract code blocks if present
                    code_blocks = extract_code_blocks(note["content"])
                    note["code_blocks"] = code_blocks
                    note["has_yaml"] = any("yaml" in block["language"].lower() for block in code_blocks)
                    results.append(note)
                    seen_ids.add(note["id"])
        
        return results[:20]  # Limit to 20 results
        
    except Exception as e:
        return [{"error": f"Error searching Kubernetes examples: {str(e)}"}]
@mcp.tool()
def find_code_examples(language: str = "", topic: str = "", limit: int = 15) -> List[Dict[str, Any]]:
    """
    Find code examples in Bear notes
    
    Args:
        language: Programming language (python, javascript, go, etc.)
        topic: Topic to search for (docker, api, database, etc.)
        limit: Maximum number of results
    
    Returns:
        Notes containing code examples with extracted code blocks
    """
    try:
        search_terms = []
        
        if language:
            search_terms.extend([
                f"```{language}",
                f"#{language}",
                language.lower()
            ])
        
        if topic:
            search_terms.append(topic.lower())
        
        # General code-related terms
        code_terms = ["```", "code", "example", "script", "function", "class"]
        
        results = []
        seen_ids = set()
        
        all_terms = search_terms + (code_terms if not search_terms else [])
        
        for term in all_terms:
            notes = search_notes(term, limit=10)
            for note in notes:
                if note["id"] not in seen_ids:
                    # Extract and analyze code blocks
                    code_blocks = extract_code_blocks(note["content"])
                    
                    # Filter code blocks by language if specified
                    if language:
                        code_blocks = [
                            block for block in code_blocks 
                            if language.lower() in block["language"].lower()
                        ]
                    
                    note["code_blocks"] = code_blocks
                    note["code_block_count"] = len(code_blocks)
                    note["languages"] = list(set(block["language"] for block in code_blocks))
                    
                    results.append(note)
                    seen_ids.add(note["id"])
        
        return results[:limit]
        
    except Exception as e:
        return [{"error": f"Error searching code examples: {str(e)}"}]
@mcp.tool()
def find_notes_by_title(title_query: str, exact_match: bool = False) -> List[Dict[str, Any]]:
    """
    Find notes by title
    
    Args:
        title_query: Title text to search for
        exact_match: Whether to match title exactly or use partial matching
    
    Returns:
        Notes matching the title criteria
    """
    try:
        conn = get_bear_db_connection()
        
        if exact_match:
            sql = """
            SELECT 
                ZUNIQUEIDENTIFIER as id,
                ZTITLE as title,
                ZTEXT as content,
                ZCREATIONDATE as created_date,
                ZMODIFICATIONDATE as modified_date
            FROM ZSFNOTE 
            WHERE ZTRASHED = 0 AND ZTITLE = ?
            ORDER BY ZMODIFICATIONDATE DESC
            """
            params = [title_query]
        else:
            sql = """
            SELECT 
                ZUNIQUEIDENTIFIER as id,
                ZTITLE as title,
                ZTEXT as content,
                ZCREATIONDATE as created_date,
                ZMODIFICATIONDATE as modified_date
            FROM ZSFNOTE 
            WHERE ZTRASHED = 0 AND ZTITLE LIKE ?
            ORDER BY ZMODIFICATIONDATE DESC
            """
            params = [f"%{title_query}%"]
        
        cursor = conn.execute(sql, params)
        results = []
        
        for row in cursor.fetchall():
            content = row["content"] or ""
            results.append({
                "id": row["id"],
                "title": row["title"] or "Untitled",
                "content": content,
                "created_date": row["created_date"],
                "modified_date": row["modified_date"],
                "preview": content[:200] + "..." if len(content) > 200 else content
            })
        
        conn.close()
        return results
        
    except Exception as e:
        return [{"error": f"Error searching by title: {str(e)}"}]
@mcp.tool()
def get_recent_notes(days: int = 7, limit: int = 20) -> List[Dict[str, Any]]:
    """
    Get recently modified notes
    
    Args:
        days: Number of days to look back
        limit: Maximum number of results
    
    Returns:
        Recently modified notes
    """
    try:
        conn = get_bear_db_connection()
        
        # Calculate timestamp for N days ago
        # Bear uses Core Data timestamps (seconds since 2001-01-01)
        import time
        import datetime
        
        now = datetime.datetime.now()
        days_ago = now - datetime.timedelta(days=days)
        
        # Convert to Core Data timestamp
        core_data_epoch = datetime.datetime(2001, 1, 1)
        timestamp = (days_ago - core_data_epoch).total_seconds()
        
        cursor = conn.execute("""
            SELECT 
                ZUNIQUEIDENTIFIER as id,
                ZTITLE as title,
                ZTEXT as content,
                ZCREATIONDATE as created_date,
                ZMODIFICATIONDATE as modified_date
            FROM ZSFNOTE 
            WHERE ZTRASHED = 0 AND ZMODIFICATIONDATE > ?
            ORDER BY ZMODIFICATIONDATE DESC
            LIMIT ?
        """, (timestamp, limit))
        
        results = []
        for row in cursor.fetchall():
            content = row["content"] or ""
            results.append({
                "id": row["id"],
                "title": row["title"] or "Untitled",
                "content": content,
                "created_date": row["created_date"],
                "modified_date": row["modified_date"],
                "preview": content[:200] + "..." if len(content) > 200 else content,
                "word_count": len(content.split()) if content else 0
            })
        
        conn.close()
        return results
        
    except Exception as e:
        return [{"error": f"Error getting recent notes: {str(e)}"}]
if __name__ == "__main__":
    # Start the server
    mcp.run()
```