# 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()
```