bear-notes-mcp 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -11
- package/dist/config.js +2 -4
- package/dist/main.js +125 -59
- package/dist/note-conventions.js +35 -0
- package/dist/note-conventions.test.js +78 -0
- package/dist/utils.js +9 -15
- package/package.json +10 -8
package/README.md
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
# Bear Notes MCP Server
|
|
2
2
|
|
|
3
|
-
Search, read, and update your Bear Notes
|
|
3
|
+
Search, read, create, and update your Bear Notes from any AI assistant.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Full documentation and source code: [claude-desktop-extension-bear-notes](https://github.com/vasylenko/claude-desktop-extension-bear-notes)**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Key Features
|
|
8
|
+
|
|
9
|
+
- **9 MCP tools** for full Bear Notes integration
|
|
10
|
+
- **OCR search** across images and PDFs attached to notes
|
|
11
|
+
- **Date-based search** with relative dates ("yesterday", "last week", etc.)
|
|
12
|
+
- **Configurable new note convention** for tag placement (opt-in)
|
|
13
|
+
- **Local-only** — no network calls, all data stays on your Mac
|
|
8
14
|
|
|
9
15
|
## Tools
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
- **`bear-open-note`** - Read full content of a
|
|
13
|
-
- **`bear-create-note`** - Create new
|
|
14
|
-
- **`bear-
|
|
15
|
-
- **`bear-add-
|
|
17
|
+
<!-- TOOLS:START -->
|
|
18
|
+
- **`bear-open-note`** - Read the full text content of a Bear note including OCR'd text from attached images and PDFs
|
|
19
|
+
- **`bear-create-note`** - Create a new note in your Bear library with optional title, content, and tags
|
|
20
|
+
- **`bear-search-notes`** - Find notes by searching text content, filtering by tags, or date ranges. Includes OCR search in attachments
|
|
21
|
+
- **`bear-add-text`** - Add text to an existing Bear note at the beginning or end, optionally targeting a specific section
|
|
22
|
+
- **`bear-add-file`** - Attach a file (image, PDF, Excel, etc.) to an existing Bear note using base64-encoded content
|
|
16
23
|
- **`bear-list-tags`** - List all tags in your Bear library as a hierarchical tree with note counts
|
|
17
|
-
- **`bear-find-untagged-notes`** - Find notes that have no tags assigned
|
|
18
|
-
- **`bear-add-tag`** - Add one or more tags to an existing note
|
|
24
|
+
- **`bear-find-untagged-notes`** - Find notes in your Bear library that have no tags assigned
|
|
25
|
+
- **`bear-add-tag`** - Add one or more tags to an existing Bear note
|
|
26
|
+
- **`bear-archive-note`** - Archive a Bear note to remove it from active lists without deleting it
|
|
27
|
+
<!-- TOOLS:END -->
|
|
19
28
|
|
|
20
|
-
**Requirements**: Node.js
|
|
29
|
+
**Requirements**: Node.js 24.13.0+
|
|
21
30
|
|
|
22
31
|
## Quick Start - Claude Code (One Command)
|
|
23
32
|
|
|
@@ -41,6 +50,29 @@ Add to your MCP configuration file:
|
|
|
41
50
|
}
|
|
42
51
|
```
|
|
43
52
|
|
|
53
|
+
## Configuration
|
|
54
|
+
|
|
55
|
+
| Variable | Default | Description |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `UI_DEBUG_TOGGLE` | `false` | Enable debug logging for troubleshooting |
|
|
58
|
+
| `UI_ENABLE_NEW_NOTE_CONVENTION` | `false` | Place tags right after the note title instead of at the bottom |
|
|
59
|
+
|
|
60
|
+
Example with configuration:
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"mcpServers": {
|
|
64
|
+
"bear-notes": {
|
|
65
|
+
"command": "npx",
|
|
66
|
+
"args": ["-y", "bear-notes-mcp@latest"],
|
|
67
|
+
"env": {
|
|
68
|
+
"UI_ENABLE_NEW_NOTE_CONVENTION": "true",
|
|
69
|
+
"UI_DEBUG_TOGGLE": "true"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
44
76
|
## Advanced: Local Development Build
|
|
45
77
|
|
|
46
78
|
**Step 1: Clone and build**
|
package/dist/config.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
export const APP_VERSION = '2.
|
|
1
|
+
export const APP_VERSION = '2.4.0';
|
|
2
2
|
export const BEAR_URL_SCHEME = 'bear://x-callback-url/';
|
|
3
3
|
export const CORE_DATA_EPOCH_OFFSET = 978307200; // 2001-01-01 to Unix epoch
|
|
4
4
|
export const DEFAULT_SEARCH_LIMIT = 50;
|
|
5
5
|
export const BEAR_DATABASE_PATH = 'Library/Group Containers/9K33E3U3T4.net.shinyfrog.bear/Application Data/database.sqlite';
|
|
6
|
+
export const ENABLE_NEW_NOTE_CONVENTIONS = process.env.UI_ENABLE_NEW_NOTE_CONVENTION === 'true';
|
|
6
7
|
export const ERROR_MESSAGES = {
|
|
7
8
|
BEAR_DATABASE_NOT_FOUND: 'Bear database not found. Please ensure Bear Notes is installed and has been opened at least once.',
|
|
8
|
-
MISSING_SEARCH_PARAM: 'Please provide either a search term or a tag to search for notes.',
|
|
9
|
-
MISSING_NOTE_ID: 'Please provide a note identifier. Use bear-search-notes first to find the note ID.',
|
|
10
|
-
MISSING_TEXT_PARAM: 'Text input parameter is required and must be a non-empty string',
|
|
11
9
|
};
|
package/dist/main.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { APP_VERSION,
|
|
5
|
+
import { APP_VERSION, ENABLE_NEW_NOTE_CONVENTIONS } from './config.js';
|
|
6
|
+
import { applyNoteConventions } from './note-conventions.js';
|
|
6
7
|
import { cleanBase64, createToolResponse, handleAddText, logger } from './utils.js';
|
|
7
8
|
import { getNoteContent, searchNotes } from './notes.js';
|
|
8
9
|
import { findUntaggedNotes, listTags } from './tags.js';
|
|
@@ -15,22 +16,23 @@ server.registerTool('bear-open-note', {
|
|
|
15
16
|
title: 'Open Bear Note',
|
|
16
17
|
description: 'Read the full text content of a Bear note from your library. Always includes text extracted from attached images and PDFs (aka OCR search) with clear labeling.',
|
|
17
18
|
inputSchema: {
|
|
18
|
-
|
|
19
|
+
id: z
|
|
20
|
+
.string()
|
|
21
|
+
.trim()
|
|
22
|
+
.min(1, 'Note ID is required')
|
|
23
|
+
.describe('Exact note identifier (ID) obtained from bear-search-notes'),
|
|
19
24
|
},
|
|
20
25
|
annotations: {
|
|
21
26
|
readOnlyHint: true,
|
|
22
27
|
idempotentHint: true,
|
|
23
28
|
openWorldHint: false,
|
|
24
29
|
},
|
|
25
|
-
}, async ({
|
|
26
|
-
logger.info(`bear-open-note called with
|
|
27
|
-
if (!identifier || !identifier.trim()) {
|
|
28
|
-
throw new Error(ERROR_MESSAGES.MISSING_NOTE_ID);
|
|
29
|
-
}
|
|
30
|
+
}, async ({ id }) => {
|
|
31
|
+
logger.info(`bear-open-note called with id: ${id}, includeFiles: always`);
|
|
30
32
|
try {
|
|
31
|
-
const noteWithContent = getNoteContent(
|
|
33
|
+
const noteWithContent = getNoteContent(id);
|
|
32
34
|
if (!noteWithContent) {
|
|
33
|
-
return createToolResponse(`Note with ID '${
|
|
35
|
+
return createToolResponse(`Note with ID '${id}' not found. The note may have been deleted, archived, or the ID may be incorrect.
|
|
34
36
|
|
|
35
37
|
Use bear-search-notes to find the correct note identifier.`);
|
|
36
38
|
}
|
|
@@ -47,7 +49,7 @@ Use bear-search-notes to find the correct note identifier.`);
|
|
|
47
49
|
${noteText}`);
|
|
48
50
|
}
|
|
49
51
|
catch (error) {
|
|
50
|
-
logger.error(
|
|
52
|
+
logger.error('bear-open-note failed:', error);
|
|
51
53
|
throw error;
|
|
52
54
|
}
|
|
53
55
|
});
|
|
@@ -57,10 +59,19 @@ server.registerTool('bear-create-note', {
|
|
|
57
59
|
inputSchema: {
|
|
58
60
|
title: z
|
|
59
61
|
.string()
|
|
62
|
+
.trim()
|
|
60
63
|
.optional()
|
|
61
64
|
.describe('Note title, e.g., "Meeting Notes" or "Research Ideas"'),
|
|
62
|
-
text: z
|
|
63
|
-
|
|
65
|
+
text: z
|
|
66
|
+
.string()
|
|
67
|
+
.trim()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('Note content in markdown format. Do not include a title heading — Bear adds it automatically from the title parameter.'),
|
|
70
|
+
tags: z
|
|
71
|
+
.string()
|
|
72
|
+
.trim()
|
|
73
|
+
.optional()
|
|
74
|
+
.describe('Tags separated by commas, e.g., "work,project,urgent"'),
|
|
64
75
|
},
|
|
65
76
|
annotations: {
|
|
66
77
|
readOnlyHint: false,
|
|
@@ -71,26 +82,30 @@ server.registerTool('bear-create-note', {
|
|
|
71
82
|
}, async ({ title, text, tags }) => {
|
|
72
83
|
logger.debug(`bear-create-note called with title: ${title ? '"' + title + '"' : 'none'}, text length: ${text ? text.length : 0}, tags: ${tags || 'none'}`);
|
|
73
84
|
try {
|
|
74
|
-
|
|
85
|
+
// If ENABLE_NOTE_CONVENTIONS is true, embed tags in the text body using Bear's inline tag syntax, rather than passing as URL parameters
|
|
86
|
+
const { text: createText, tags: createTags } = ENABLE_NEW_NOTE_CONVENTIONS
|
|
87
|
+
? applyNoteConventions({ text, tags })
|
|
88
|
+
: { text, tags };
|
|
89
|
+
const url = buildBearUrl('create', { title, text: createText, tags: createTags });
|
|
75
90
|
await executeBearXCallbackApi(url);
|
|
76
91
|
const responseLines = ['Bear note created successfully!', ''];
|
|
77
|
-
if (title
|
|
78
|
-
responseLines.push(`Title: "${title
|
|
92
|
+
if (title) {
|
|
93
|
+
responseLines.push(`Title: "${title}"`);
|
|
79
94
|
}
|
|
80
|
-
if (text
|
|
81
|
-
responseLines.push(`Content: ${text.
|
|
95
|
+
if (text) {
|
|
96
|
+
responseLines.push(`Content: ${text.length} characters`);
|
|
82
97
|
}
|
|
83
|
-
if (tags
|
|
84
|
-
responseLines.push(`Tags: ${tags
|
|
98
|
+
if (tags) {
|
|
99
|
+
responseLines.push(`Tags: ${tags}`);
|
|
85
100
|
}
|
|
86
|
-
const hasContent = title
|
|
101
|
+
const hasContent = title || text || tags;
|
|
87
102
|
const finalMessage = hasContent ? responseLines.join('\n') : 'Empty note created';
|
|
88
103
|
return createToolResponse(`${finalMessage}
|
|
89
104
|
|
|
90
105
|
The note has been added to your Bear Notes library.`);
|
|
91
106
|
}
|
|
92
107
|
catch (error) {
|
|
93
|
-
logger.error(
|
|
108
|
+
logger.error('bear-create-note failed:', error);
|
|
94
109
|
throw error;
|
|
95
110
|
}
|
|
96
111
|
});
|
|
@@ -98,8 +113,8 @@ server.registerTool('bear-search-notes', {
|
|
|
98
113
|
title: 'Find Bear Notes',
|
|
99
114
|
description: 'Find notes in your Bear library by searching text content, filtering by tags, or date ranges. Always searches within attached images and PDF files via OCR. Returns a list with titles and IDs - use "Open Bear Note" to read full content.',
|
|
100
115
|
inputSchema: {
|
|
101
|
-
term: z.string().optional().describe('Text to search for in note titles and content'),
|
|
102
|
-
tag: z.string().optional().describe('Tag to filter notes by (without # symbol)'),
|
|
116
|
+
term: z.string().trim().optional().describe('Text to search for in note titles and content'),
|
|
117
|
+
tag: z.string().trim().optional().describe('Tag to filter notes by (without # symbol)'),
|
|
103
118
|
limit: z.number().optional().describe('Maximum number of results to return (default: 50)'),
|
|
104
119
|
createdAfter: z
|
|
105
120
|
.string()
|
|
@@ -139,10 +154,10 @@ server.registerTool('bear-search-notes', {
|
|
|
139
154
|
const { notes, totalCount } = searchNotes(term, tag, limit, Object.keys(dateFilter).length > 0 ? dateFilter : undefined, pinned);
|
|
140
155
|
if (notes.length === 0) {
|
|
141
156
|
const searchCriteria = [];
|
|
142
|
-
if (term
|
|
143
|
-
searchCriteria.push(`term "${term
|
|
144
|
-
if (tag
|
|
145
|
-
searchCriteria.push(`tag "${tag
|
|
157
|
+
if (term)
|
|
158
|
+
searchCriteria.push(`term "${term}"`);
|
|
159
|
+
if (tag)
|
|
160
|
+
searchCriteria.push(`tag "${tag}"`);
|
|
146
161
|
if (createdAfter)
|
|
147
162
|
searchCriteria.push(`created after "${createdAfter}"`);
|
|
148
163
|
if (createdBefore)
|
|
@@ -180,7 +195,7 @@ Try different search criteria or check if notes exist in Bear Notes.`);
|
|
|
180
195
|
return createToolResponse(resultLines.join('\n'));
|
|
181
196
|
}
|
|
182
197
|
catch (error) {
|
|
183
|
-
logger.error(
|
|
198
|
+
logger.error('bear-search-notes failed:', error);
|
|
184
199
|
throw error;
|
|
185
200
|
}
|
|
186
201
|
});
|
|
@@ -188,10 +203,19 @@ server.registerTool('bear-add-text', {
|
|
|
188
203
|
title: 'Add Text to Note',
|
|
189
204
|
description: 'Add text to an existing Bear note at the beginning or end. Can target a specific section using header. Use bear-search-notes first to get the note ID.',
|
|
190
205
|
inputSchema: {
|
|
191
|
-
id: z
|
|
192
|
-
|
|
206
|
+
id: z
|
|
207
|
+
.string()
|
|
208
|
+
.trim()
|
|
209
|
+
.min(1, 'Note ID is required')
|
|
210
|
+
.describe('Note identifier (ID) from bear-search-notes'),
|
|
211
|
+
text: z
|
|
212
|
+
.string()
|
|
213
|
+
.trim()
|
|
214
|
+
.min(1, 'Text content is required')
|
|
215
|
+
.describe('Text content to add to the note'),
|
|
193
216
|
header: z
|
|
194
217
|
.string()
|
|
218
|
+
.trim()
|
|
195
219
|
.optional()
|
|
196
220
|
.describe('Optional section header to target (adds text within that section)'),
|
|
197
221
|
position: z
|
|
@@ -213,13 +237,22 @@ server.registerTool('bear-add-file', {
|
|
|
213
237
|
title: 'Add File to Note',
|
|
214
238
|
description: 'Attach a file to an existing Bear note. Encode the file to base64 using shell commands (e.g., base64 /path/to/file.xlsx) and provide the encoded content. Use bear-search-notes first to get the note ID.',
|
|
215
239
|
inputSchema: {
|
|
216
|
-
base64_content: z
|
|
217
|
-
|
|
240
|
+
base64_content: z
|
|
241
|
+
.string()
|
|
242
|
+
.trim()
|
|
243
|
+
.min(1, 'Base64 file content is required')
|
|
244
|
+
.describe('Base64-encoded file content'),
|
|
245
|
+
filename: z
|
|
246
|
+
.string()
|
|
247
|
+
.trim()
|
|
248
|
+
.min(1, 'Filename is required')
|
|
249
|
+
.describe('Filename with extension (e.g., budget.xlsx, report.pdf)'),
|
|
218
250
|
id: z
|
|
219
251
|
.string()
|
|
252
|
+
.trim()
|
|
220
253
|
.optional()
|
|
221
254
|
.describe('Exact note identifier (ID) obtained from bear-search-notes'),
|
|
222
|
-
title: z.string().optional().describe('Note title if ID is not available'),
|
|
255
|
+
title: z.string().trim().optional().describe('Note title if ID is not available'),
|
|
223
256
|
},
|
|
224
257
|
annotations: {
|
|
225
258
|
readOnlyHint: false,
|
|
@@ -229,12 +262,6 @@ server.registerTool('bear-add-file', {
|
|
|
229
262
|
},
|
|
230
263
|
}, async ({ base64_content, filename, id, title }) => {
|
|
231
264
|
logger.info(`bear-add-file called with base64_content: ${base64_content ? 'provided' : 'none'}, filename: ${filename || 'none'}, id: ${id || 'none'}, title: ${title || 'none'}`);
|
|
232
|
-
if (!base64_content || !base64_content.trim()) {
|
|
233
|
-
throw new Error('base64_content is required');
|
|
234
|
-
}
|
|
235
|
-
if (!filename || !filename.trim()) {
|
|
236
|
-
throw new Error('filename is required');
|
|
237
|
-
}
|
|
238
265
|
if (!id && !title) {
|
|
239
266
|
throw new Error('Either note ID or title is required. Use bear-search-notes to find the note ID.');
|
|
240
267
|
}
|
|
@@ -243,7 +270,7 @@ server.registerTool('bear-add-file', {
|
|
|
243
270
|
const cleanedBase64 = cleanBase64(base64_content);
|
|
244
271
|
// Fail fast with helpful message rather than cryptic Bear error
|
|
245
272
|
if (id) {
|
|
246
|
-
const existingNote = getNoteContent(id
|
|
273
|
+
const existingNote = getNoteContent(id);
|
|
247
274
|
if (!existingNote) {
|
|
248
275
|
return createToolResponse(`Note with ID '${id}' not found. The note may have been deleted, archived, or the ID may be incorrect.
|
|
249
276
|
|
|
@@ -251,23 +278,23 @@ Use bear-search-notes to find the correct note identifier.`);
|
|
|
251
278
|
}
|
|
252
279
|
}
|
|
253
280
|
const url = buildBearUrl('add-file', {
|
|
254
|
-
id
|
|
255
|
-
title
|
|
281
|
+
id,
|
|
282
|
+
title,
|
|
256
283
|
file: cleanedBase64,
|
|
257
|
-
filename
|
|
284
|
+
filename,
|
|
258
285
|
mode: 'append',
|
|
259
286
|
});
|
|
260
|
-
logger.debug(`Executing Bear add-file URL for: ${filename
|
|
287
|
+
logger.debug(`Executing Bear add-file URL for: ${filename}`);
|
|
261
288
|
await executeBearXCallbackApi(url);
|
|
262
|
-
const noteIdentifier = id ? `Note ID: ${id
|
|
263
|
-
return createToolResponse(`File "${filename
|
|
289
|
+
const noteIdentifier = id ? `Note ID: ${id}` : `Note title: "${title}"`;
|
|
290
|
+
return createToolResponse(`File "${filename}" added successfully!
|
|
264
291
|
|
|
265
292
|
${noteIdentifier}
|
|
266
293
|
|
|
267
294
|
The file has been attached to your Bear note.`);
|
|
268
295
|
}
|
|
269
296
|
catch (error) {
|
|
270
|
-
logger.error(
|
|
297
|
+
logger.error('bear-add-file failed:', error);
|
|
271
298
|
throw error;
|
|
272
299
|
}
|
|
273
300
|
});
|
|
@@ -321,7 +348,7 @@ server.registerTool('bear-list-tags', {
|
|
|
321
348
|
return createToolResponse(header + '\n' + lines.join('\n'));
|
|
322
349
|
}
|
|
323
350
|
catch (error) {
|
|
324
|
-
logger.error(
|
|
351
|
+
logger.error('bear-list-tags failed:', error);
|
|
325
352
|
throw error;
|
|
326
353
|
}
|
|
327
354
|
});
|
|
@@ -363,7 +390,7 @@ server.registerTool('bear-find-untagged-notes', {
|
|
|
363
390
|
return createToolResponse(lines.join('\n'));
|
|
364
391
|
}
|
|
365
392
|
catch (error) {
|
|
366
|
-
logger.error(
|
|
393
|
+
logger.error('bear-find-untagged-notes failed:', error);
|
|
367
394
|
throw error;
|
|
368
395
|
}
|
|
369
396
|
});
|
|
@@ -373,9 +400,12 @@ server.registerTool('bear-add-tag', {
|
|
|
373
400
|
inputSchema: {
|
|
374
401
|
id: z
|
|
375
402
|
.string()
|
|
403
|
+
.trim()
|
|
404
|
+
.min(1, 'Note ID is required')
|
|
376
405
|
.describe('Note identifier (ID) from bear-search-notes or bear-find-untagged-notes'),
|
|
377
406
|
tags: z
|
|
378
|
-
.array(z.string())
|
|
407
|
+
.array(z.string().trim().min(1, 'Tag name cannot be empty'))
|
|
408
|
+
.min(1, 'At least one tag is required')
|
|
379
409
|
.describe('Tag names without # symbol (e.g., ["career", "career/meetings"])'),
|
|
380
410
|
},
|
|
381
411
|
annotations: {
|
|
@@ -386,14 +416,8 @@ server.registerTool('bear-add-tag', {
|
|
|
386
416
|
},
|
|
387
417
|
}, async ({ id, tags }) => {
|
|
388
418
|
logger.info(`bear-add-tag called with id: ${id}, tags: [${tags.join(', ')}]`);
|
|
389
|
-
if (!id || !id.trim()) {
|
|
390
|
-
throw new Error(ERROR_MESSAGES.MISSING_NOTE_ID);
|
|
391
|
-
}
|
|
392
|
-
if (!tags || tags.length === 0) {
|
|
393
|
-
throw new Error('At least one tag is required');
|
|
394
|
-
}
|
|
395
419
|
try {
|
|
396
|
-
const existingNote = getNoteContent(id
|
|
420
|
+
const existingNote = getNoteContent(id);
|
|
397
421
|
if (!existingNote) {
|
|
398
422
|
return createToolResponse(`Note with ID '${id}' not found. The note may have been deleted, archived, or the ID may be incorrect.
|
|
399
423
|
|
|
@@ -401,7 +425,7 @@ Use bear-search-notes to find the correct note identifier.`);
|
|
|
401
425
|
}
|
|
402
426
|
const tagsString = tags.join(',');
|
|
403
427
|
const url = buildBearUrl('add-text', {
|
|
404
|
-
id
|
|
428
|
+
id,
|
|
405
429
|
tags: tagsString,
|
|
406
430
|
mode: 'prepend',
|
|
407
431
|
open_note: 'no',
|
|
@@ -418,7 +442,49 @@ Tags: ${tagList}
|
|
|
418
442
|
The tags have been added to the beginning of the note.`);
|
|
419
443
|
}
|
|
420
444
|
catch (error) {
|
|
421
|
-
logger.error(
|
|
445
|
+
logger.error('bear-add-tag failed:', error);
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
server.registerTool('bear-archive-note', {
|
|
450
|
+
title: 'Archive Bear Note',
|
|
451
|
+
description: "Move a note to Bear's archive. The note will no longer appear in regular searches but can be found in Bear's Archive section. Use bear-search-notes first to get the note ID.",
|
|
452
|
+
inputSchema: {
|
|
453
|
+
id: z
|
|
454
|
+
.string()
|
|
455
|
+
.trim()
|
|
456
|
+
.min(1, 'Note ID is required')
|
|
457
|
+
.describe('Note identifier (ID) from bear-search-notes or bear-open-note'),
|
|
458
|
+
},
|
|
459
|
+
annotations: {
|
|
460
|
+
readOnlyHint: false,
|
|
461
|
+
destructiveHint: true,
|
|
462
|
+
idempotentHint: true,
|
|
463
|
+
openWorldHint: true,
|
|
464
|
+
},
|
|
465
|
+
}, async ({ id }) => {
|
|
466
|
+
logger.info(`bear-archive-note called with id: ${id}`);
|
|
467
|
+
try {
|
|
468
|
+
const existingNote = getNoteContent(id);
|
|
469
|
+
if (!existingNote) {
|
|
470
|
+
return createToolResponse(`Note with ID '${id}' not found. The note may have been deleted, archived, or the ID may be incorrect.
|
|
471
|
+
|
|
472
|
+
Use bear-search-notes to find the correct note identifier.`);
|
|
473
|
+
}
|
|
474
|
+
const url = buildBearUrl('archive', {
|
|
475
|
+
id,
|
|
476
|
+
show_window: 'no',
|
|
477
|
+
});
|
|
478
|
+
await executeBearXCallbackApi(url);
|
|
479
|
+
return createToolResponse(`Note archived successfully!
|
|
480
|
+
|
|
481
|
+
Note: "${existingNote.title}"
|
|
482
|
+
ID: ${id}
|
|
483
|
+
|
|
484
|
+
The note has been moved to Bear's archive.`);
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
logger.error('bear-archive-note failed:', error);
|
|
422
488
|
throw error;
|
|
423
489
|
}
|
|
424
490
|
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies note creation conventions by embedding tags as Bear inline syntax
|
|
3
|
+
* at the start of the text body, rather than passing them as URL parameters
|
|
4
|
+
* (which places them at the bottom of the note).
|
|
5
|
+
*/
|
|
6
|
+
export function applyNoteConventions(input) {
|
|
7
|
+
if (!input.tags) {
|
|
8
|
+
return { text: input.text, tags: undefined };
|
|
9
|
+
}
|
|
10
|
+
const tagLine = input.tags
|
|
11
|
+
.split(',')
|
|
12
|
+
.map((t) => t.trim())
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
.map(toBearTagSyntax)
|
|
15
|
+
.filter(Boolean)
|
|
16
|
+
.join(' ');
|
|
17
|
+
// All tags were invalid (e.g., "###,,,") — pass text through unchanged
|
|
18
|
+
if (!tagLine) {
|
|
19
|
+
return { text: input.text, tags: undefined };
|
|
20
|
+
}
|
|
21
|
+
const text = input.text ? `${tagLine}\n---\n${input.text}` : tagLine;
|
|
22
|
+
return { text, tags: undefined };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Bear uses `#tag` for simple tags and `#tag#` (closing hash) for
|
|
26
|
+
* multi-word tags containing spaces. Slashes create hierarchy without
|
|
27
|
+
* requiring a closing hash.
|
|
28
|
+
*/
|
|
29
|
+
function toBearTagSyntax(raw) {
|
|
30
|
+
const cleaned = raw.replace(/^#+|#+$/g, '').trim();
|
|
31
|
+
if (!cleaned)
|
|
32
|
+
return '';
|
|
33
|
+
const needsClosingHash = cleaned.includes(' ');
|
|
34
|
+
return needsClosingHash ? `#${cleaned}#` : `#${cleaned}`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { applyNoteConventions } from './note-conventions.js';
|
|
3
|
+
describe('applyNoteConventions', () => {
|
|
4
|
+
describe('pass-through when no tags provided', () => {
|
|
5
|
+
it('undefined tags returns text unchanged', () => {
|
|
6
|
+
const result = applyNoteConventions({ text: 'hello', tags: undefined });
|
|
7
|
+
expect(result).toEqual({ text: 'hello', tags: undefined });
|
|
8
|
+
});
|
|
9
|
+
it('empty string tags returns text unchanged', () => {
|
|
10
|
+
const result = applyNoteConventions({ text: 'hello', tags: '' });
|
|
11
|
+
expect(result).toEqual({ text: 'hello', tags: undefined });
|
|
12
|
+
});
|
|
13
|
+
it('both text and tags undefined returns both unchanged', () => {
|
|
14
|
+
const result = applyNoteConventions({ text: undefined, tags: undefined });
|
|
15
|
+
expect(result).toEqual({ text: undefined, tags: undefined });
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
describe('tags only, no text', () => {
|
|
19
|
+
it('multiple tags produce tag line without separator', () => {
|
|
20
|
+
const result = applyNoteConventions({ text: undefined, tags: 'work,urgent' });
|
|
21
|
+
expect(result).toEqual({ text: '#work #urgent', tags: undefined });
|
|
22
|
+
});
|
|
23
|
+
it('empty string text treated as no text', () => {
|
|
24
|
+
const result = applyNoteConventions({ text: '', tags: 'work' });
|
|
25
|
+
expect(result).toEqual({ text: '#work', tags: undefined });
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe('tags + text composition', () => {
|
|
29
|
+
it('multiple tags and text joined with separator', () => {
|
|
30
|
+
const result = applyNoteConventions({ text: 'body', tags: 'work,urgent' });
|
|
31
|
+
expect(result).toEqual({ text: '#work #urgent\n---\nbody', tags: undefined });
|
|
32
|
+
});
|
|
33
|
+
it('single tag and text joined with separator', () => {
|
|
34
|
+
const result = applyNoteConventions({ text: 'body', tags: 'work' });
|
|
35
|
+
expect(result).toEqual({ text: '#work\n---\nbody', tags: undefined });
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe('closing hash rules', () => {
|
|
39
|
+
it('nested tag without spaces has no closing hash', () => {
|
|
40
|
+
const result = applyNoteConventions({ text: undefined, tags: 'work/meetings' });
|
|
41
|
+
expect(result).toEqual({ text: '#work/meetings', tags: undefined });
|
|
42
|
+
});
|
|
43
|
+
it('tag with space gets closing hash', () => {
|
|
44
|
+
const result = applyNoteConventions({ text: undefined, tags: 'my tag' });
|
|
45
|
+
expect(result).toEqual({ text: '#my tag#', tags: undefined });
|
|
46
|
+
});
|
|
47
|
+
it('nested tag with spaces gets closing hash', () => {
|
|
48
|
+
const result = applyNoteConventions({ text: undefined, tags: 'work/meeting notes' });
|
|
49
|
+
expect(result).toEqual({ text: '#work/meeting notes#', tags: undefined });
|
|
50
|
+
});
|
|
51
|
+
it('simple tag has no closing hash', () => {
|
|
52
|
+
const result = applyNoteConventions({ text: undefined, tags: 'urgent' });
|
|
53
|
+
expect(result).toEqual({ text: '#urgent', tags: undefined });
|
|
54
|
+
});
|
|
55
|
+
it('mixed tags apply closing hash per-tag', () => {
|
|
56
|
+
const result = applyNoteConventions({ text: undefined, tags: 'work/meetings,urgent,my tag' });
|
|
57
|
+
expect(result).toEqual({ text: '#work/meetings #urgent #my tag#', tags: undefined });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe('tag cleanup edge cases', () => {
|
|
61
|
+
it('strips leading and trailing hash symbols from tags', () => {
|
|
62
|
+
const result = applyNoteConventions({ text: undefined, tags: '#work,##urgent#' });
|
|
63
|
+
expect(result).toEqual({ text: '#work #urgent', tags: undefined });
|
|
64
|
+
});
|
|
65
|
+
it('all-invalid tags pass text through unchanged', () => {
|
|
66
|
+
const result = applyNoteConventions({ text: 'hello', tags: '###,,, ' });
|
|
67
|
+
expect(result).toEqual({ text: 'hello', tags: undefined });
|
|
68
|
+
});
|
|
69
|
+
it('empty segments between commas are filtered out', () => {
|
|
70
|
+
const result = applyNoteConventions({ text: undefined, tags: 'work, , ,urgent' });
|
|
71
|
+
expect(result).toEqual({ text: '#work #urgent', tags: undefined });
|
|
72
|
+
});
|
|
73
|
+
it('whitespace around tags is trimmed', () => {
|
|
74
|
+
const result = applyNoteConventions({ text: undefined, tags: ' work , urgent ' });
|
|
75
|
+
expect(result).toEqual({ text: '#work #urgent', tags: undefined });
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
package/dist/utils.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import createDebug from 'debug';
|
|
2
|
-
import { CORE_DATA_EPOCH_OFFSET
|
|
2
|
+
import { CORE_DATA_EPOCH_OFFSET } from './config.js';
|
|
3
3
|
import { getNoteContent } from './notes.js';
|
|
4
4
|
import { buildBearUrl, executeBearXCallbackApi } from './bear-urls.js';
|
|
5
5
|
export const logger = {
|
|
@@ -144,35 +144,29 @@ export function createToolResponse(text) {
|
|
|
144
144
|
export async function handleAddText(mode, { id, text, header }) {
|
|
145
145
|
const action = mode === 'append' ? 'appended' : 'prepended';
|
|
146
146
|
logger.info(`bear-add-text-${mode} called with id: ${id}, text length: ${text.length}, header: ${header || 'none'}`);
|
|
147
|
-
if (!id || !id.trim()) {
|
|
148
|
-
throw new Error(ERROR_MESSAGES.MISSING_NOTE_ID);
|
|
149
|
-
}
|
|
150
|
-
if (!text || !text.trim()) {
|
|
151
|
-
throw new Error(ERROR_MESSAGES.MISSING_TEXT_PARAM);
|
|
152
|
-
}
|
|
153
147
|
try {
|
|
154
|
-
const existingNote = getNoteContent(id
|
|
148
|
+
const existingNote = getNoteContent(id);
|
|
155
149
|
if (!existingNote) {
|
|
156
150
|
return createToolResponse(`Note with ID '${id}' not found. The note may have been deleted, archived, or the ID may be incorrect.
|
|
157
151
|
|
|
158
152
|
Use bear-search-notes to find the correct note identifier.`);
|
|
159
153
|
}
|
|
160
154
|
// Strip markdown header syntax from header parameter for Bear API
|
|
161
|
-
const cleanHeader = header?.
|
|
155
|
+
const cleanHeader = header?.replace(/^#+\s*/, '');
|
|
162
156
|
const url = buildBearUrl('add-text', {
|
|
163
|
-
id
|
|
164
|
-
text
|
|
157
|
+
id,
|
|
158
|
+
text,
|
|
165
159
|
header: cleanHeader,
|
|
166
160
|
mode,
|
|
167
161
|
});
|
|
168
162
|
logger.debug(`Executing Bear URL: ${url}`);
|
|
169
163
|
await executeBearXCallbackApi(url);
|
|
170
164
|
const responseLines = [`Text ${action} to note "${existingNote.title}" successfully!`, ''];
|
|
171
|
-
responseLines.push(`Text: ${text.
|
|
172
|
-
if (header
|
|
173
|
-
responseLines.push(`Section: ${header
|
|
165
|
+
responseLines.push(`Text: ${text.length} characters`);
|
|
166
|
+
if (header) {
|
|
167
|
+
responseLines.push(`Section: ${header}`);
|
|
174
168
|
}
|
|
175
|
-
responseLines.push(`Note ID: ${id
|
|
169
|
+
responseLines.push(`Note ID: ${id}`);
|
|
176
170
|
return createToolResponse(`${responseLines.join('\n')}
|
|
177
171
|
|
|
178
172
|
The text has been added to your Bear note.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bear-notes-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Bear Notes MCP server with TypeScript and native SQLite",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"dev": "tsx src/main.ts --debug",
|
|
10
10
|
"start": "node dist/main.js",
|
|
11
11
|
"test": "vitest run",
|
|
12
|
+
"test:system": "npm run build && vitest run --config vitest.system.config.ts",
|
|
12
13
|
"test:watch": "vitest",
|
|
13
14
|
"lint": "eslint src --fix",
|
|
14
15
|
"lint:check": "eslint src",
|
|
@@ -19,23 +20,24 @@
|
|
|
19
20
|
"prepublishOnly": "npm run build && mv docs/NPM.md README.md"
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
22
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
23
24
|
"debug": "^4.4.3",
|
|
24
|
-
"zod": "^4.3.
|
|
25
|
+
"zod": "^4.3.6",
|
|
25
26
|
"zod-to-json-schema": "^3.25.1"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"@anthropic-ai/mcpb": "^2.1.2",
|
|
30
|
+
"@modelcontextprotocol/inspector": "^0.20.0",
|
|
29
31
|
"@types/debug": "^4.1.12",
|
|
30
|
-
"@types/node": "^24.10.
|
|
31
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
32
|
-
"@typescript-eslint/parser": "^8.
|
|
32
|
+
"@types/node": "^24.10.13",
|
|
33
|
+
"@typescript-eslint/eslint-plugin": "^8.55.0",
|
|
34
|
+
"@typescript-eslint/parser": "^8.55.0",
|
|
33
35
|
"eslint": "^9.39.2",
|
|
34
36
|
"eslint-plugin-import": "^2.32.0",
|
|
35
|
-
"prettier": "^3.8.
|
|
37
|
+
"prettier": "^3.8.1",
|
|
36
38
|
"tsx": "^4.21.0",
|
|
37
39
|
"typescript": "^5.9.3",
|
|
38
|
-
"vitest": "^4.0.
|
|
40
|
+
"vitest": "^4.0.18"
|
|
39
41
|
},
|
|
40
42
|
"engines": {
|
|
41
43
|
"node": ">=24.13.0"
|