bear-notes-mcp 2.8.1 → 2.9.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 +5 -5
- package/dist/config.js +1 -1
- package/dist/main.js +97 -23
- package/dist/notes.js +51 -0
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Search, read, create, and update your Bear Notes from any AI assistant.
|
|
4
4
|
|
|
5
|
-
**Full documentation and source code: [
|
|
5
|
+
**Full documentation and source code: [bear-notes-mcp](https://github.com/vasylenko/bear-notes-mcp)**
|
|
6
6
|
|
|
7
7
|
## Key Features
|
|
8
8
|
|
|
9
|
-
- **
|
|
9
|
+
- **12 MCP tools** for full Bear Notes integration
|
|
10
10
|
- **OCR search** across images and PDFs attached to notes
|
|
11
11
|
- **Date-based search** with relative dates ("yesterday", "last week", etc.)
|
|
12
12
|
- **Configurable new note convention** for tag placement (opt-in)
|
|
@@ -21,7 +21,7 @@ Search, read, create, and update your Bear Notes from any AI assistant.
|
|
|
21
21
|
- **`bear-search-notes`** - Find notes by searching text content, filtering by tags, or date ranges. Includes OCR search in attachments
|
|
22
22
|
- **`bear-add-text`** - Insert text at the beginning or end of a Bear note, or within a specific section identified by its header
|
|
23
23
|
- **`bear-replace-text`** - Replace content in an existing Bear note — either the full body or a specific section. Requires content replacement to be enabled in settings.
|
|
24
|
-
- **`bear-add-file`** - Attach a file
|
|
24
|
+
- **`bear-add-file`** - Attach a file to an existing Bear note. Provide a local file path (preferred) or base64-encoded content.
|
|
25
25
|
- **`bear-list-tags`** - List all tags in your Bear library as a hierarchical tree with note counts
|
|
26
26
|
- **`bear-find-untagged-notes`** - Find notes in your Bear library that have no tags assigned
|
|
27
27
|
- **`bear-add-tag`** - Add one or more tags to an existing Bear note
|
|
@@ -83,8 +83,8 @@ Example with configuration:
|
|
|
83
83
|
|
|
84
84
|
**Step 1: Clone and build**
|
|
85
85
|
```bash
|
|
86
|
-
git clone https://github.com/vasylenko/
|
|
87
|
-
cd
|
|
86
|
+
git clone https://github.com/vasylenko/bear-notes-mcp.git
|
|
87
|
+
cd bear-notes-mcp
|
|
88
88
|
npm install
|
|
89
89
|
npm run build
|
|
90
90
|
```
|
package/dist/config.js
CHANGED
package/dist/main.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { basename } from 'node:path';
|
|
2
4
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
6
|
import { z } from 'zod';
|
|
5
7
|
import { APP_VERSION, ENABLE_CONTENT_REPLACEMENT, ENABLE_NEW_NOTE_CONVENTIONS } from './config.js';
|
|
6
8
|
import { applyNoteConventions } from './note-conventions.js';
|
|
7
9
|
import { cleanBase64, createToolResponse, handleNoteTextUpdate, logger } from './utils.js';
|
|
8
|
-
import { awaitNoteCreation, getNoteContent, searchNotes } from './notes.js';
|
|
10
|
+
import { awaitNoteCreation, findNotesByTitle, getNoteContent, searchNotes } from './notes.js';
|
|
9
11
|
import { findUntaggedNotes, listTags } from './tags.js';
|
|
10
12
|
import { buildBearUrl, executeBearXCallbackApi } from './bear-urls.js';
|
|
11
13
|
const server = new McpServer({
|
|
@@ -16,7 +18,7 @@ const server = new McpServer({
|
|
|
16
18
|
'This server integrates with Bear, a markdown note-taking app.',
|
|
17
19
|
'Each note has a unique ID, a title, a body, and optional tags.',
|
|
18
20
|
'Notes use markdown headings (##, ###, etc.) to define sections.',
|
|
19
|
-
'Use bear-search-notes to find note IDs before reading or modifying notes.',
|
|
21
|
+
'Use bear-search-notes to find note IDs before reading or modifying notes, or provide an exact title to bear-open-note for direct lookup.',
|
|
20
22
|
'To modify note content: bear-add-text inserts text without touching existing content; bear-replace-text overwrites content.',
|
|
21
23
|
'When targeting a section by header, operations apply only to the direct content under that header — not nested sub-sections.',
|
|
22
24
|
'To modify sub-sections, make separate calls targeting each sub-header.',
|
|
@@ -24,22 +26,51 @@ const server = new McpServer({
|
|
|
24
26
|
});
|
|
25
27
|
server.registerTool('bear-open-note', {
|
|
26
28
|
title: 'Open Bear Note',
|
|
27
|
-
description: 'Read the full text content of a Bear note
|
|
29
|
+
description: 'Read the full text content of a Bear note by its ID or title. Supports direct title lookup as an alternative to searching first. Always includes text extracted from attached images and PDFs (aka OCR search) with clear labeling.',
|
|
28
30
|
inputSchema: {
|
|
29
31
|
id: z
|
|
30
32
|
.string()
|
|
31
33
|
.trim()
|
|
32
|
-
.
|
|
33
|
-
.describe('
|
|
34
|
+
.optional()
|
|
35
|
+
.describe('Note identifier (ID) from bear-search-notes. Either id or title must be provided.'),
|
|
36
|
+
title: z
|
|
37
|
+
.string()
|
|
38
|
+
.trim()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe('Exact note title for direct lookup (case-insensitive). Either id or title must be provided. If multiple notes share the same title, returns a list for disambiguation.'),
|
|
34
41
|
},
|
|
35
42
|
annotations: {
|
|
36
43
|
readOnlyHint: true,
|
|
37
44
|
idempotentHint: true,
|
|
38
45
|
openWorldHint: false,
|
|
39
46
|
},
|
|
40
|
-
}, async ({ id }) => {
|
|
41
|
-
logger.info(`bear-open-note called with id: ${id},
|
|
47
|
+
}, async ({ id, title }) => {
|
|
48
|
+
logger.info(`bear-open-note called with id: ${id || 'none'}, title: ${title ? '"' + title + '"' : 'none'}`);
|
|
49
|
+
if (!id && !title) {
|
|
50
|
+
return createToolResponse('Either note ID or title is required. Use bear-search-notes to find the note ID, or provide the exact title.');
|
|
51
|
+
}
|
|
42
52
|
try {
|
|
53
|
+
// Title lookup path: find by title, then fetch full content
|
|
54
|
+
if (!id && title) {
|
|
55
|
+
const matches = findNotesByTitle(title);
|
|
56
|
+
if (matches.length === 0) {
|
|
57
|
+
return createToolResponse(`No note found with title "${title}". The note may have been deleted, archived, or the title may be different.
|
|
58
|
+
|
|
59
|
+
Use bear-search-notes to find notes by partial text match.`);
|
|
60
|
+
}
|
|
61
|
+
if (matches.length > 1) {
|
|
62
|
+
const matchList = matches
|
|
63
|
+
.map((m, i) => `${i + 1}. ID: ${m.identifier} (modified: ${m.modification_date})`)
|
|
64
|
+
.join('\n');
|
|
65
|
+
return createToolResponse(`Multiple notes found with title "${title}":
|
|
66
|
+
|
|
67
|
+
${matchList}
|
|
68
|
+
|
|
69
|
+
Use bear-open-note with a specific ID to open the desired note.`);
|
|
70
|
+
}
|
|
71
|
+
// Exactly one match — fetch full content by ID
|
|
72
|
+
id = matches[0].identifier;
|
|
73
|
+
}
|
|
43
74
|
const noteWithContent = getNoteContent(id);
|
|
44
75
|
if (!noteWithContent) {
|
|
45
76
|
return createToolResponse(`Note with ID '${id}' not found. The note may have been deleted, archived, or the ID may be incorrect.
|
|
@@ -262,7 +293,7 @@ server.registerTool('bear-add-text', {
|
|
|
262
293
|
});
|
|
263
294
|
server.registerTool('bear-replace-text', {
|
|
264
295
|
title: 'Replace Note Content',
|
|
265
|
-
description: 'Replace content in an existing Bear note — either the full body or a specific section. Requires content replacement to be enabled in
|
|
296
|
+
description: 'Replace content in an existing Bear note — either the full body or a specific section. Requires content replacement to be enabled in settings. Use bear-search-notes first to get the note ID. To add text without replacing existing content use bear-add-text instead.',
|
|
266
297
|
inputSchema: {
|
|
267
298
|
id: z
|
|
268
299
|
.string()
|
|
@@ -293,7 +324,7 @@ server.registerTool('bear-replace-text', {
|
|
|
293
324
|
if (!ENABLE_CONTENT_REPLACEMENT) {
|
|
294
325
|
return createToolResponse(`Content replacement is not enabled.
|
|
295
326
|
|
|
296
|
-
To use replace mode, enable "Content Replacement" in the Bear Notes
|
|
327
|
+
To use replace mode, enable "Content Replacement" in the Bear Notes server settings.`);
|
|
297
328
|
}
|
|
298
329
|
if (scope === 'section' && !header) {
|
|
299
330
|
return createToolResponse(`scope is "section" but no header was provided.
|
|
@@ -309,18 +340,26 @@ Remove the header parameter to replace the full note body, or change scope to "s
|
|
|
309
340
|
});
|
|
310
341
|
server.registerTool('bear-add-file', {
|
|
311
342
|
title: 'Add File to Note',
|
|
312
|
-
description: 'Attach a file to an existing Bear note.
|
|
343
|
+
description: 'Attach a file to an existing Bear note. Preferred: provide file_path for files on disk — the server reads and encodes them automatically. Alternative: provide base64_content with pre-encoded data. Use bear-search-notes first to get the note ID.',
|
|
313
344
|
inputSchema: {
|
|
345
|
+
file_path: z
|
|
346
|
+
.string()
|
|
347
|
+
.trim()
|
|
348
|
+
.min(1)
|
|
349
|
+
.optional()
|
|
350
|
+
.describe('Path to a file on disk. Preferred over base64_content when the file already exists locally.'),
|
|
314
351
|
base64_content: z
|
|
315
352
|
.string()
|
|
316
353
|
.trim()
|
|
317
|
-
.min(1
|
|
318
|
-
.
|
|
354
|
+
.min(1)
|
|
355
|
+
.optional()
|
|
356
|
+
.describe('Base64-encoded file content. Use file_path instead when the file exists on disk.'),
|
|
319
357
|
filename: z
|
|
320
358
|
.string()
|
|
321
359
|
.trim()
|
|
322
|
-
.min(1
|
|
323
|
-
.
|
|
360
|
+
.min(1)
|
|
361
|
+
.optional()
|
|
362
|
+
.describe('Filename with extension (e.g., budget.xlsx, report.pdf). Required when using base64_content. Auto-inferred from file_path when omitted.'),
|
|
324
363
|
id: z
|
|
325
364
|
.string()
|
|
326
365
|
.trim()
|
|
@@ -334,14 +373,49 @@ server.registerTool('bear-add-file', {
|
|
|
334
373
|
idempotentHint: false,
|
|
335
374
|
openWorldHint: true,
|
|
336
375
|
},
|
|
337
|
-
}, async ({ base64_content, filename, id, title }) => {
|
|
338
|
-
logger.info(`bear-add-file called with base64_content: ${base64_content ? 'provided' : 'none'}, filename: ${filename || 'none'}, id: ${id || 'none'}, title: ${title || 'none'}`);
|
|
376
|
+
}, async ({ file_path, base64_content, filename, id, title }) => {
|
|
377
|
+
logger.info(`bear-add-file called with file_path: ${file_path || 'none'}, base64_content: ${base64_content ? 'provided' : 'none'}, filename: ${filename || 'none'}, id: ${id || 'none'}, title: ${title || 'none'}`);
|
|
339
378
|
if (!id && !title) {
|
|
340
|
-
|
|
379
|
+
return createToolResponse('Either note ID or title is required. Use bear-search-notes to find the note ID.');
|
|
380
|
+
}
|
|
381
|
+
if (file_path && base64_content) {
|
|
382
|
+
return createToolResponse('Provide either file_path or base64_content, not both.');
|
|
383
|
+
}
|
|
384
|
+
if (!file_path && !base64_content) {
|
|
385
|
+
return createToolResponse('Either file_path or base64_content is required.');
|
|
386
|
+
}
|
|
387
|
+
if (base64_content && !filename) {
|
|
388
|
+
return createToolResponse('filename is required when using base64_content.');
|
|
341
389
|
}
|
|
342
390
|
try {
|
|
343
|
-
|
|
344
|
-
|
|
391
|
+
let fileData;
|
|
392
|
+
let resolvedFilename;
|
|
393
|
+
if (file_path) {
|
|
394
|
+
// Read file from disk and encode — avoids the LLM producing thousands of base64 tokens
|
|
395
|
+
try {
|
|
396
|
+
const buffer = readFileSync(file_path);
|
|
397
|
+
if (buffer.length === 0) {
|
|
398
|
+
return createToolResponse(`File is empty: ${file_path}`);
|
|
399
|
+
}
|
|
400
|
+
fileData = buffer.toString('base64');
|
|
401
|
+
}
|
|
402
|
+
catch (err) {
|
|
403
|
+
const code = err.code;
|
|
404
|
+
if (code === 'ENOENT') {
|
|
405
|
+
return createToolResponse(`File not found: ${file_path}`);
|
|
406
|
+
}
|
|
407
|
+
if (code === 'EACCES') {
|
|
408
|
+
return createToolResponse(`Permission denied: ${file_path}`);
|
|
409
|
+
}
|
|
410
|
+
return createToolResponse(`Cannot read file: ${err instanceof Error ? err.message : String(err)}`);
|
|
411
|
+
}
|
|
412
|
+
resolvedFilename = filename || basename(file_path);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
// base64_content path — strip whitespace that base64 CLI adds
|
|
416
|
+
fileData = cleanBase64(base64_content);
|
|
417
|
+
resolvedFilename = filename;
|
|
418
|
+
}
|
|
345
419
|
// Fail fast with helpful message rather than cryptic Bear error
|
|
346
420
|
if (id) {
|
|
347
421
|
const existingNote = getNoteContent(id);
|
|
@@ -354,14 +428,14 @@ Use bear-search-notes to find the correct note identifier.`);
|
|
|
354
428
|
const url = buildBearUrl('add-file', {
|
|
355
429
|
id,
|
|
356
430
|
title,
|
|
357
|
-
file:
|
|
358
|
-
filename,
|
|
431
|
+
file: fileData,
|
|
432
|
+
filename: resolvedFilename,
|
|
359
433
|
mode: 'append',
|
|
360
434
|
});
|
|
361
|
-
logger.debug(`Executing Bear add-file URL for: ${
|
|
435
|
+
logger.debug(`Executing Bear add-file URL for: ${resolvedFilename}`);
|
|
362
436
|
await executeBearXCallbackApi(url);
|
|
363
437
|
const noteIdentifier = id ? `Note ID: ${id}` : `Note title: "${title}"`;
|
|
364
|
-
return createToolResponse(`File "${
|
|
438
|
+
return createToolResponse(`File "${resolvedFilename}" added successfully!
|
|
365
439
|
|
|
366
440
|
${noteIdentifier}
|
|
367
441
|
|
package/dist/notes.js
CHANGED
|
@@ -121,6 +121,57 @@ export function getNoteContent(identifier) {
|
|
|
121
121
|
}
|
|
122
122
|
return null;
|
|
123
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Finds Bear notes matching an exact title (case-insensitive).
|
|
126
|
+
* Returns lightweight match objects for disambiguation — call getNoteContent()
|
|
127
|
+
* with the chosen identifier to retrieve the full note.
|
|
128
|
+
*
|
|
129
|
+
* @param title - The exact note title to match
|
|
130
|
+
* @returns Array of matching notes (empty if none found)
|
|
131
|
+
* @throws Error if database access fails
|
|
132
|
+
*/
|
|
133
|
+
export function findNotesByTitle(title) {
|
|
134
|
+
logger.info(`findNotesByTitle called with title: "${title}"`);
|
|
135
|
+
if (!title || typeof title !== 'string' || !title.trim()) {
|
|
136
|
+
logAndThrow('Database error: Invalid note title provided');
|
|
137
|
+
}
|
|
138
|
+
const db = openBearDatabase();
|
|
139
|
+
try {
|
|
140
|
+
const query = `
|
|
141
|
+
SELECT ZTITLE as title,
|
|
142
|
+
ZUNIQUEIDENTIFIER as identifier,
|
|
143
|
+
ZMODIFICATIONDATE as modificationDate
|
|
144
|
+
FROM ZSFNOTE
|
|
145
|
+
WHERE ZTITLE = ? COLLATE NOCASE
|
|
146
|
+
AND ZARCHIVED = 0
|
|
147
|
+
AND ZTRASHED = 0
|
|
148
|
+
AND ZENCRYPTED = 0
|
|
149
|
+
ORDER BY ZMODIFICATIONDATE DESC
|
|
150
|
+
`;
|
|
151
|
+
const stmt = db.prepare(query);
|
|
152
|
+
const rows = stmt.all(title.trim());
|
|
153
|
+
if (!rows || rows.length === 0) {
|
|
154
|
+
logger.info(`No notes found with title: "${title}"`);
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
logger.info(`Found ${rows.length} note(s) with title: "${title}"`);
|
|
158
|
+
return rows.map((row) => {
|
|
159
|
+
const r = row;
|
|
160
|
+
return {
|
|
161
|
+
identifier: r.identifier,
|
|
162
|
+
title: r.title || 'Untitled',
|
|
163
|
+
modification_date: convertCoreDataTimestamp(r.modificationDate),
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
logAndThrow(`Database error: Failed to find notes by title: ${error instanceof Error ? error.message : String(error)}`);
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
closeBearDatabase(db);
|
|
172
|
+
}
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
124
175
|
/**
|
|
125
176
|
* Searches Bear notes by content or tags with optional filtering.
|
|
126
177
|
* Returns a list of notes without full content for performance.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bear-notes-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "Bear Notes MCP server with TypeScript and native SQLite",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -49,7 +49,9 @@
|
|
|
49
49
|
"notes management",
|
|
50
50
|
"productivity",
|
|
51
51
|
"typescript",
|
|
52
|
-
"mcp"
|
|
52
|
+
"mcp",
|
|
53
|
+
"mcp-server",
|
|
54
|
+
"ai-assistant"
|
|
53
55
|
],
|
|
54
56
|
"author": {
|
|
55
57
|
"name": "Serhii Vasylenko",
|
|
@@ -59,12 +61,12 @@
|
|
|
59
61
|
"license": "MIT",
|
|
60
62
|
"repository": {
|
|
61
63
|
"type": "git",
|
|
62
|
-
"url": "git+https://github.com/vasylenko/
|
|
64
|
+
"url": "git+https://github.com/vasylenko/bear-notes-mcp.git"
|
|
63
65
|
},
|
|
64
66
|
"bugs": {
|
|
65
|
-
"url": "https://github.com/vasylenko/
|
|
67
|
+
"url": "https://github.com/vasylenko/bear-notes-mcp/issues"
|
|
66
68
|
},
|
|
67
|
-
"homepage": "https://github.com/vasylenko/
|
|
69
|
+
"homepage": "https://github.com/vasylenko/bear-notes-mcp#readme",
|
|
68
70
|
"bin": {
|
|
69
71
|
"bear-notes-mcp": "dist/main.js"
|
|
70
72
|
},
|