#
tokens: 6084/50000 5/5 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── .python-version
├── main.py
├── pyproject.toml
├── README.md
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
1 | 3.13
2 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Python-generated files
 2 | __pycache__/
 3 | *.py[oc]
 4 | build/
 5 | dist/
 6 | wheels/
 7 | *.egg-info
 8 | 
 9 | # Virtual environments
10 | .venv
11 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Bear App MCP Server
  2 | 
  3 | 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.
  4 | 
  5 | ## Features
  6 | 
  7 | ### 🔍 Search & Discovery
  8 | - **Full-text search** across all your notes
  9 | - **Tag-based filtering** for organized content
 10 | - **Title-based search** with exact or partial matching
 11 | - **Recent notes** filtering by modification date
 12 | 
 13 | ### 💻 Code & Technical Content
 14 | - **Kubernetes manifest finder** - Locate deployment examples, service configs, etc.
 15 | - **Code example search** - Find code snippets by programming language
 16 | - **Code block extraction** - Automatically parse and categorize code blocks
 17 | - **Technical documentation** discovery
 18 | 
 19 | ### 📊 Content Analysis
 20 | - **Word count** and content statistics
 21 | - **Code language detection** from fenced code blocks
 22 | - **Content previews** for quick scanning
 23 | - **Metadata extraction** (creation/modification dates)
 24 | 
 25 | ## Installation
 26 | 
 27 | ### Prerequisites
 28 | - macOS (Bear App is macOS/iOS only)
 29 | - Bear App installed and with some notes
 30 | - Python 3.8 or higher
 31 | - Claude Desktop (for desktop integration)
 32 | 
 33 | ### Setup Steps
 34 | 
 35 | 1. **Clone or download the server script**
 36 |    ```bash
 37 |    git clone github.com/netologist/mcp-bear-notes
 38 |    ```
 39 | 
 40 | 2. **Create a virtual environment**
 41 |    ```bash
 42 |    uv install
 43 |    source .venv/bin/activate
 44 |    ```
 45 | 
 46 | 4. **Test the server**
 47 |    ```bash
 48 |    uv run python main.py
 49 |    ```
 50 | 
 51 | ## Claude Desktop Integration
 52 | 
 53 | ### Configuration File Location
 54 | Edit your Claude Desktop configuration file:
 55 | ```
 56 | ~/Library/Application Support/Claude/claude_desktop_config.json
 57 | ```
 58 | 
 59 | ### Add MCP Server Configuration
 60 | ```json
 61 | {
 62 |   "mcpServers": {
 63 |     "bear-notes": {
 64 |       "command": "/full/path/to/mcp-bear-notes/.venv/bin/python",
 65 |       "args": ["/full/path/to/mcp-bear-notes/main.py"],
 66 |       "env": {
 67 |         "PYTHONPATH": "/full/path/to/mcp-bear-notes/.venv/lib/python3.13/site-packages"
 68 |       }
 69 |     }
 70 |   }
 71 | }
 72 | ```
 73 | 
 74 | **Important**: Replace `/full/path/to/` with your actual file paths.
 75 | 
 76 | ### Restart Claude Desktop
 77 | Close and reopen Claude Desktop to load the MCP server.
 78 | 
 79 | ## Usage Examples
 80 | 
 81 | Once integrated with Claude Desktop, you can use natural language to interact with your Bear notes:
 82 | 
 83 | ### Basic Search
 84 | ```
 85 | "Search my Bear notes for Docker examples"
 86 | "Find notes about Python APIs"
 87 | "Show me my recent notes from this week"
 88 | ```
 89 | 
 90 | ### Technical Content
 91 | ```
 92 | "Find my Kubernetes deployment manifests"
 93 | "Look for JavaScript code examples in my notes"
 94 | "Show me notes with YAML configurations"
 95 | ```
 96 | 
 97 | ### Specific Retrieval
 98 | ```
 99 | "Get the note titled 'Development Setup'"
100 | "Find notes tagged with 'work'"
101 | "Show me all my available tags"
102 | ```
103 | 
104 | ## Available Tools
105 | 
106 | ### `search_bear_notes(query, tag, limit)`
107 | Search notes by content and tags.
108 | - `query`: Text to search for
109 | - `tag`: Filter by specific tag (without #)
110 | - `limit`: Max results (default: 20)
111 | 
112 | ### `get_bear_note(note_id)`
113 | Retrieve a specific note by its unique ID.
114 | - `note_id`: Bear note's unique identifier
115 | 
116 | ### `find_kubernetes_examples(resource_type)`
117 | Find Kubernetes-related content.
118 | - `resource_type`: K8s resource (deployment, service, etc.)
119 | 
120 | ### `find_code_examples(language, topic, limit)`
121 | Search for code examples.
122 | - `language`: Programming language
123 | - `topic`: Code topic/domain
124 | - `limit`: Max results (default: 15)
125 | 
126 | ### `find_notes_by_title(title_query, exact_match)`
127 | Search notes by title.
128 | - `title_query`: Title text to search
129 | - `exact_match`: Exact or partial matching
130 | 
131 | ### `get_recent_notes(days, limit)`
132 | Get recently modified notes.
133 | - `days`: How many days to look back (default: 7)
134 | - `limit`: Max results (default: 20)
135 | 
136 | ### `list_bear_tags()`
137 | List all tags found in your notes.
138 | 
139 | ## Bear Database Information
140 | 
141 | The server reads from Bear's SQLite database located at:
142 | ```
143 | ~/Library/Group Containers/9K33E3U3T4.net.shinyfrog.bear/Application Data/database.sqlite
144 | ```
145 | 
146 | ### Database Access Notes
147 | - **Read-only access** - The server never modifies your notes
148 | - **No authentication required** - Uses direct SQLite access
149 | - **Performance** - Database queries are optimized for speed
150 | - **Safety** - Only accesses non-trashed notes
151 | 
152 | ## Troubleshooting
153 | 
154 | ### Common Issues
155 | 
156 | **Server won't start**
157 | - Check Python path in configuration
158 | - Verify virtual environment activation
159 | - Ensure fastmcp is installed
160 | 
161 | **No notes found**
162 | - Verify Bear database path exists
163 | - Check that Bear App has been opened at least once
164 | - Confirm notes aren't in trash
165 | 
166 | **Claude Desktop integration fails**
167 | - Validate JSON syntax in config file
168 | - Check file paths are absolute, not relative
169 | - Restart Claude Desktop after configuration changes
170 | 
171 | **Permission denied errors**
172 | - Ensure script has execute permissions: `chmod +x main.py`
173 | - Check Bear database file permissions
174 | 
175 | ### Debug Mode
176 | Run the server directly to see debug output:
177 | ```bash
178 | python main.py
179 | ```
180 | 
181 | ### Log Files
182 | Check Claude Desktop logs for MCP server errors:
183 | ```bash
184 | ~/Library/Logs/Claude/
185 | ```
186 | 
187 | ## Security & Privacy
188 | 
189 | - **Local-only**: All data stays on your machine
190 | - **Read-only**: Server never modifies your notes
191 | - **No network**: No external connections required
192 | - **Open source**: Full transparency of operations
193 | 
194 | ## Contributing
195 | 
196 | Contributions welcome! Areas for improvement:
197 | - Additional search filters
198 | - Export functionality
199 | - Note creation capabilities
200 | - iOS Shortcuts integration
201 | - Performance optimizations
202 | 
203 | ## License
204 | 
205 | MIT License - See LICENSE file for details.
206 | 
207 | ## Changelog
208 | 
209 | ### v1.0.0
210 | - Initial release
211 | - Basic search and retrieval functions
212 | - Kubernetes and code example finders
213 | - Claude Desktop integration
214 | - Tag listing and filtering
215 | 
216 | ## Support
217 | 
218 | For issues and questions:
219 | 1. Check the troubleshooting section
220 | 2. Review Claude Desktop MCP documentation
221 | 3. Open an issue on GitHub
222 | 4. Check Bear App forums for database-related questions
223 | 
224 | ---
225 | 
226 | **Note**: This is an unofficial tool and is not affiliated with Bear App or Anthropic. Use at your own discretion.
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "mcp-bear-notes"
 3 | version = "0.1.0"
 4 | description = "Add your description here"
 5 | readme = "README.md"
 6 | requires-python = ">=3.13"
 7 | dependencies = [
 8 |     "fastmcp>=2.8.1",
 9 | ]
10 | 
```

--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Bear App MCP Server
  4 | MCP server providing access to Bear App notes using FastMCP
  5 | """
  6 | 
  7 | import sqlite3
  8 | import os
  9 | from pathlib import Path
 10 | from typing import List, Dict, Any, Optional
 11 | from fastmcp import FastMCP
 12 | 
 13 | # Bear App database path (macOS)
 14 | BEAR_DB_PATH = os.path.expanduser("~/Library/Group Containers/9K33E3U3T4.net.shinyfrog.bear/Application Data/database.sqlite")
 15 | 
 16 | # Initialize MCP server
 17 | mcp = FastMCP("Bear Notes")
 18 | 
 19 | def get_bear_db_connection():
 20 |     """Connect to Bear database"""
 21 |     if not os.path.exists(BEAR_DB_PATH):
 22 |         raise FileNotFoundError(f"Bear database not found: {BEAR_DB_PATH}")
 23 |     
 24 |     conn = sqlite3.connect(BEAR_DB_PATH)
 25 |     conn.row_factory = sqlite3.Row  # Enable column name access
 26 |     return conn
 27 | 
 28 | def search_notes(query: str = "", tag: str = "", limit: int = 20) -> List[Dict[str, Any]]:
 29 |     """Search Bear notes"""
 30 |     conn = get_bear_db_connection()
 31 |     
 32 |     try:
 33 |         # Base query
 34 |         sql = """
 35 |         SELECT 
 36 |             ZUNIQUEIDENTIFIER as id,
 37 |             ZTITLE as title,
 38 |             ZTEXT as content,
 39 |             ZCREATIONDATE as created_date,
 40 |             ZMODIFICATIONDATE as modified_date,
 41 |             ZTRASHED as is_trashed
 42 |         FROM ZSFNOTE 
 43 |         WHERE ZTRASHED = 0
 44 |         """
 45 |         
 46 |         params = []
 47 |         
 48 |         # Add search criteria
 49 |         if query:
 50 |             sql += " AND (ZTITLE LIKE ? OR ZTEXT LIKE ?)"
 51 |             params.extend([f"%{query}%", f"%{query}%"])
 52 |         
 53 |         # Add tag filter
 54 |         if tag:
 55 |             sql += " AND ZTEXT LIKE ?"
 56 |             params.append(f"%#{tag}%")
 57 |         
 58 |         sql += " ORDER BY ZMODIFICATIONDATE DESC LIMIT ?"
 59 |         params.append(limit)
 60 |         
 61 |         cursor = conn.execute(sql, params)
 62 |         results = []
 63 |         
 64 |         for row in cursor.fetchall():
 65 |             content = row["content"] or ""
 66 |             results.append({
 67 |                 "id": row["id"],
 68 |                 "title": row["title"] or "Untitled",
 69 |                 "content": content,
 70 |                 "created_date": row["created_date"],
 71 |                 "modified_date": row["modified_date"],
 72 |                 "preview": content[:200] + "..." if len(content) > 200 else content,
 73 |                 "word_count": len(content.split()) if content else 0
 74 |             })
 75 |         
 76 |         return results
 77 |         
 78 |     finally:
 79 |         conn.close()
 80 | 
 81 | def get_note_by_id(note_id: str) -> Optional[Dict[str, Any]]:
 82 |     """Get a specific note by ID"""
 83 |     conn = get_bear_db_connection()
 84 |     
 85 |     try:
 86 |         cursor = conn.execute("""
 87 |             SELECT 
 88 |                 ZUNIQUEIDENTIFIER as id,
 89 |                 ZTITLE as title,
 90 |                 ZTEXT as content,
 91 |                 ZCREATIONDATE as created_date,
 92 |                 ZMODIFICATIONDATE as modified_date
 93 |             FROM ZSFNOTE 
 94 |             WHERE ZUNIQUEIDENTIFIER = ? AND ZTRASHED = 0
 95 |         """, (note_id,))
 96 |         
 97 |         row = cursor.fetchone()
 98 |         if row:
 99 |             content = row["content"] or ""
100 |             return {
101 |                 "id": row["id"],
102 |                 "title": row["title"] or "Untitled",
103 |                 "content": content,
104 |                 "created_date": row["created_date"],
105 |                 "modified_date": row["modified_date"],
106 |                 "word_count": len(content.split()) if content else 0
107 |             }
108 |         return None
109 |         
110 |     finally:
111 |         conn.close()
112 | 
113 | def get_tags() -> List[str]:
114 |     """List all tags from notes"""
115 |     conn = get_bear_db_connection()
116 |     
117 |     try:
118 |         cursor = conn.execute("""
119 |             SELECT ZTEXT 
120 |             FROM ZSFNOTE 
121 |             WHERE ZTRASHED = 0 AND ZTEXT IS NOT NULL
122 |         """)
123 |         
124 |         tags = set()
125 |         for row in cursor.fetchall():
126 |             content = row[0] or ""
127 |             # Simple tag extraction (#tag format)
128 |             import re
129 |             found_tags = re.findall(r'#(\w+)', content)
130 |             tags.update(found_tags)
131 |         
132 |         return sorted(list(tags))
133 |         
134 |     finally:
135 |         conn.close()
136 | 
137 | def extract_code_blocks(content: str) -> List[Dict[str, str]]:
138 |     """Extract code blocks from note content"""
139 |     import re
140 |     
141 |     # Find code blocks with language specification
142 |     code_blocks = []
143 |     pattern = r'```(\w+)?\n(.*?)```'
144 |     matches = re.findall(pattern, content, re.DOTALL)
145 |     
146 |     for language, code in matches:
147 |         code_blocks.append({
148 |             "language": language or "text",
149 |             "code": code.strip()
150 |         })
151 |     
152 |     return code_blocks
153 | 
154 | @mcp.tool()
155 | def search_bear_notes(query: str = "", tag: str = "", limit: int = 20) -> List[Dict[str, Any]]:
156 |     """
157 |     Search Bear App notes
158 |     
159 |     Args:
160 |         query: Text to search for (searches in title and content)
161 |         tag: Tag to filter by (without # symbol)
162 |         limit: Maximum number of results
163 |     
164 |     Returns:
165 |         List of matching notes with metadata
166 |     """
167 |     try:
168 |         return search_notes(query, tag, limit)
169 |     except Exception as e:
170 |         return [{"error": f"Search error: {str(e)}"}]
171 | 
172 | @mcp.tool()
173 | def get_bear_note(note_id: str) -> Dict[str, Any]:
174 |     """
175 |     Get a specific Bear note by ID
176 |     
177 |     Args:
178 |         note_id: Bear note's unique identifier
179 |     
180 |     Returns:
181 |         Complete note content with metadata
182 |     """
183 |     try:
184 |         note = get_note_by_id(note_id)
185 |         if note:
186 |             return note
187 |         else:
188 |             return {"error": "Note not found"}
189 |     except Exception as e:
190 |         return {"error": f"Error retrieving note: {str(e)}"}
191 | 
192 | @mcp.tool()
193 | def list_bear_tags() -> List[str]:
194 |     """
195 |     List all tags from Bear App notes
196 |     
197 |     Returns:
198 |         Sorted list of all tags found in notes
199 |     """
200 |     try:
201 |         return get_tags()
202 |     except Exception as e:
203 |         return [f"Error listing tags: {str(e)}"]
204 | 
205 | @mcp.tool()
206 | def find_kubernetes_examples(resource_type: str = "deployment") -> List[Dict[str, Any]]:
207 |     """
208 |     Find Kubernetes manifest examples in Bear notes
209 |     
210 |     Args:
211 |         resource_type: Kubernetes resource type to search for (deployment, service, configmap, etc.)
212 |     
213 |     Returns:
214 |         Notes containing Kubernetes examples
215 |     """
216 |     try:
217 |         # Search for Kubernetes-related terms
218 |         k8s_terms = [
219 |             f"kind: {resource_type.title()}",
220 |             f"apiVersion:",
221 |             f"kubernetes {resource_type}",
222 |             f"k8s {resource_type}",
223 |             f"kubectl",
224 |             "yaml",
225 |             "manifest"
226 |         ]
227 |         
228 |         results = []
229 |         seen_ids = set()
230 |         
231 |         for term in k8s_terms:
232 |             notes = search_notes(term, limit=10)
233 |             for note in notes:
234 |                 if note["id"] not in seen_ids:
235 |                     # Extract code blocks if present
236 |                     code_blocks = extract_code_blocks(note["content"])
237 |                     note["code_blocks"] = code_blocks
238 |                     note["has_yaml"] = any("yaml" in block["language"].lower() for block in code_blocks)
239 |                     results.append(note)
240 |                     seen_ids.add(note["id"])
241 |         
242 |         return results[:20]  # Limit to 20 results
243 |         
244 |     except Exception as e:
245 |         return [{"error": f"Error searching Kubernetes examples: {str(e)}"}]
246 | 
247 | @mcp.tool()
248 | def find_code_examples(language: str = "", topic: str = "", limit: int = 15) -> List[Dict[str, Any]]:
249 |     """
250 |     Find code examples in Bear notes
251 |     
252 |     Args:
253 |         language: Programming language (python, javascript, go, etc.)
254 |         topic: Topic to search for (docker, api, database, etc.)
255 |         limit: Maximum number of results
256 |     
257 |     Returns:
258 |         Notes containing code examples with extracted code blocks
259 |     """
260 |     try:
261 |         search_terms = []
262 |         
263 |         if language:
264 |             search_terms.extend([
265 |                 f"```{language}",
266 |                 f"#{language}",
267 |                 language.lower()
268 |             ])
269 |         
270 |         if topic:
271 |             search_terms.append(topic.lower())
272 |         
273 |         # General code-related terms
274 |         code_terms = ["```", "code", "example", "script", "function", "class"]
275 |         
276 |         results = []
277 |         seen_ids = set()
278 |         
279 |         all_terms = search_terms + (code_terms if not search_terms else [])
280 |         
281 |         for term in all_terms:
282 |             notes = search_notes(term, limit=10)
283 |             for note in notes:
284 |                 if note["id"] not in seen_ids:
285 |                     # Extract and analyze code blocks
286 |                     code_blocks = extract_code_blocks(note["content"])
287 |                     
288 |                     # Filter code blocks by language if specified
289 |                     if language:
290 |                         code_blocks = [
291 |                             block for block in code_blocks 
292 |                             if language.lower() in block["language"].lower()
293 |                         ]
294 |                     
295 |                     note["code_blocks"] = code_blocks
296 |                     note["code_block_count"] = len(code_blocks)
297 |                     note["languages"] = list(set(block["language"] for block in code_blocks))
298 |                     
299 |                     results.append(note)
300 |                     seen_ids.add(note["id"])
301 |         
302 |         return results[:limit]
303 |         
304 |     except Exception as e:
305 |         return [{"error": f"Error searching code examples: {str(e)}"}]
306 | 
307 | @mcp.tool()
308 | def find_notes_by_title(title_query: str, exact_match: bool = False) -> List[Dict[str, Any]]:
309 |     """
310 |     Find notes by title
311 |     
312 |     Args:
313 |         title_query: Title text to search for
314 |         exact_match: Whether to match title exactly or use partial matching
315 |     
316 |     Returns:
317 |         Notes matching the title criteria
318 |     """
319 |     try:
320 |         conn = get_bear_db_connection()
321 |         
322 |         if exact_match:
323 |             sql = """
324 |             SELECT 
325 |                 ZUNIQUEIDENTIFIER as id,
326 |                 ZTITLE as title,
327 |                 ZTEXT as content,
328 |                 ZCREATIONDATE as created_date,
329 |                 ZMODIFICATIONDATE as modified_date
330 |             FROM ZSFNOTE 
331 |             WHERE ZTRASHED = 0 AND ZTITLE = ?
332 |             ORDER BY ZMODIFICATIONDATE DESC
333 |             """
334 |             params = [title_query]
335 |         else:
336 |             sql = """
337 |             SELECT 
338 |                 ZUNIQUEIDENTIFIER as id,
339 |                 ZTITLE as title,
340 |                 ZTEXT as content,
341 |                 ZCREATIONDATE as created_date,
342 |                 ZMODIFICATIONDATE as modified_date
343 |             FROM ZSFNOTE 
344 |             WHERE ZTRASHED = 0 AND ZTITLE LIKE ?
345 |             ORDER BY ZMODIFICATIONDATE DESC
346 |             """
347 |             params = [f"%{title_query}%"]
348 |         
349 |         cursor = conn.execute(sql, params)
350 |         results = []
351 |         
352 |         for row in cursor.fetchall():
353 |             content = row["content"] or ""
354 |             results.append({
355 |                 "id": row["id"],
356 |                 "title": row["title"] or "Untitled",
357 |                 "content": content,
358 |                 "created_date": row["created_date"],
359 |                 "modified_date": row["modified_date"],
360 |                 "preview": content[:200] + "..." if len(content) > 200 else content
361 |             })
362 |         
363 |         conn.close()
364 |         return results
365 |         
366 |     except Exception as e:
367 |         return [{"error": f"Error searching by title: {str(e)}"}]
368 | 
369 | @mcp.tool()
370 | def get_recent_notes(days: int = 7, limit: int = 20) -> List[Dict[str, Any]]:
371 |     """
372 |     Get recently modified notes
373 |     
374 |     Args:
375 |         days: Number of days to look back
376 |         limit: Maximum number of results
377 |     
378 |     Returns:
379 |         Recently modified notes
380 |     """
381 |     try:
382 |         conn = get_bear_db_connection()
383 |         
384 |         # Calculate timestamp for N days ago
385 |         # Bear uses Core Data timestamps (seconds since 2001-01-01)
386 |         import time
387 |         import datetime
388 |         
389 |         now = datetime.datetime.now()
390 |         days_ago = now - datetime.timedelta(days=days)
391 |         
392 |         # Convert to Core Data timestamp
393 |         core_data_epoch = datetime.datetime(2001, 1, 1)
394 |         timestamp = (days_ago - core_data_epoch).total_seconds()
395 |         
396 |         cursor = conn.execute("""
397 |             SELECT 
398 |                 ZUNIQUEIDENTIFIER as id,
399 |                 ZTITLE as title,
400 |                 ZTEXT as content,
401 |                 ZCREATIONDATE as created_date,
402 |                 ZMODIFICATIONDATE as modified_date
403 |             FROM ZSFNOTE 
404 |             WHERE ZTRASHED = 0 AND ZMODIFICATIONDATE > ?
405 |             ORDER BY ZMODIFICATIONDATE DESC
406 |             LIMIT ?
407 |         """, (timestamp, limit))
408 |         
409 |         results = []
410 |         for row in cursor.fetchall():
411 |             content = row["content"] or ""
412 |             results.append({
413 |                 "id": row["id"],
414 |                 "title": row["title"] or "Untitled",
415 |                 "content": content,
416 |                 "created_date": row["created_date"],
417 |                 "modified_date": row["modified_date"],
418 |                 "preview": content[:200] + "..." if len(content) > 200 else content,
419 |                 "word_count": len(content.split()) if content else 0
420 |             })
421 |         
422 |         conn.close()
423 |         return results
424 |         
425 |     except Exception as e:
426 |         return [{"error": f"Error getting recent notes: {str(e)}"}]
427 | 
428 | if __name__ == "__main__":
429 |     # Start the server
430 |     mcp.run()
```