Skip to main content
Glama

Bear Notes MCP Server

by netologist
main.py13.2 kB
#!/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()

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/netologist/mcp-bear-notes'

If you have feedback or need assistance with the MCP directory API, please join our Discord server