bear-notes-mcp 2.5.0 → 2.7.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 +1 -1
- package/dist/config.js +1 -1
- package/dist/database.js +13 -0
- package/dist/main.js +21 -10
- package/dist/notes.js +56 -15
- package/dist/tags.js +3 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Search, read, create, and update your Bear Notes from any AI assistant.
|
|
|
19
19
|
- **`bear-open-note`** - Read the full text content of a Bear note including OCR'd text from attached images and PDFs
|
|
20
20
|
- **`bear-create-note`** - Create a new note in your Bear library with optional title, content, and tags
|
|
21
21
|
- **`bear-search-notes`** - Find notes by searching text content, filtering by tags, or date ranges. Includes OCR search in attachments
|
|
22
|
-
- **`bear-add-text`** -
|
|
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
24
|
- **`bear-add-file`** - Attach a file (image, PDF, Excel, etc.) to an existing Bear note using base64-encoded content
|
|
25
25
|
- **`bear-list-tags`** - List all tags in your Bear library as a hierarchical tree with note counts
|
package/dist/config.js
CHANGED
package/dist/database.js
CHANGED
|
@@ -19,6 +19,19 @@ function getBearDatabasePath() {
|
|
|
19
19
|
logger.debug(`Using default Bear database path: ${defaultPath}`);
|
|
20
20
|
return defaultPath;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Closes a Bear database connection, logging but swallowing close errors.
|
|
24
|
+
* Centralizes the try/catch-close pattern used after every DB operation.
|
|
25
|
+
*/
|
|
26
|
+
export function closeBearDatabase(db) {
|
|
27
|
+
try {
|
|
28
|
+
db.close();
|
|
29
|
+
logger.debug('Database connection closed');
|
|
30
|
+
}
|
|
31
|
+
catch (closeError) {
|
|
32
|
+
logger.error(`Failed to close database connection: ${closeError}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
22
35
|
/**
|
|
23
36
|
* Opens a read-only connection to Bear's SQLite database.
|
|
24
37
|
*
|
package/dist/main.js
CHANGED
|
@@ -5,12 +5,22 @@ import { z } from 'zod';
|
|
|
5
5
|
import { APP_VERSION, ENABLE_CONTENT_REPLACEMENT, ENABLE_NEW_NOTE_CONVENTIONS } from './config.js';
|
|
6
6
|
import { applyNoteConventions } from './note-conventions.js';
|
|
7
7
|
import { cleanBase64, createToolResponse, handleNoteTextUpdate, logger } from './utils.js';
|
|
8
|
-
import { getNoteContent, searchNotes } from './notes.js';
|
|
8
|
+
import { awaitNoteCreation, getNoteContent, searchNotes } from './notes.js';
|
|
9
9
|
import { findUntaggedNotes, listTags } from './tags.js';
|
|
10
10
|
import { buildBearUrl, executeBearXCallbackApi } from './bear-urls.js';
|
|
11
11
|
const server = new McpServer({
|
|
12
12
|
name: 'bear-notes-mcp',
|
|
13
13
|
version: APP_VERSION,
|
|
14
|
+
}, {
|
|
15
|
+
instructions: [
|
|
16
|
+
'This server integrates with Bear, a markdown note-taking app.',
|
|
17
|
+
'Each note has a unique ID, a title, a body, and optional tags.',
|
|
18
|
+
'Notes use markdown headings (##, ###, etc.) to define sections.',
|
|
19
|
+
'Use bear-search-notes to find note IDs before reading or modifying notes.',
|
|
20
|
+
'To modify note content: bear-add-text inserts text without touching existing content; bear-replace-text overwrites content.',
|
|
21
|
+
'When targeting a section by header, operations apply only to the direct content under that header — not nested sub-sections.',
|
|
22
|
+
'To modify sub-sections, make separate calls targeting each sub-header.',
|
|
23
|
+
].join('\n'),
|
|
14
24
|
});
|
|
15
25
|
server.registerTool('bear-open-note', {
|
|
16
26
|
title: 'Open Bear Note',
|
|
@@ -55,7 +65,7 @@ ${noteText}`);
|
|
|
55
65
|
});
|
|
56
66
|
server.registerTool('bear-create-note', {
|
|
57
67
|
title: 'Create New Note',
|
|
58
|
-
description: 'Create a new note in your Bear library with optional title, content, and tags. The note will be immediately available in Bear app.',
|
|
68
|
+
description: 'Create a new note in your Bear library with optional title, content, and tags. Returns the note ID when a title is provided, enabling immediate follow-up operations. The note will be immediately available in Bear app.',
|
|
59
69
|
inputSchema: {
|
|
60
70
|
title: z
|
|
61
71
|
.string()
|
|
@@ -88,16 +98,17 @@ server.registerTool('bear-create-note', {
|
|
|
88
98
|
: { text, tags };
|
|
89
99
|
const url = buildBearUrl('create', { title, text: createText, tags: createTags });
|
|
90
100
|
await executeBearXCallbackApi(url);
|
|
101
|
+
const createdNoteId = title ? await awaitNoteCreation(title) : undefined;
|
|
91
102
|
const responseLines = ['Bear note created successfully!', ''];
|
|
92
103
|
if (title) {
|
|
93
104
|
responseLines.push(`Title: "${title}"`);
|
|
94
105
|
}
|
|
95
|
-
if (text) {
|
|
96
|
-
responseLines.push(`Content: ${text.length} characters`);
|
|
97
|
-
}
|
|
98
106
|
if (tags) {
|
|
99
107
|
responseLines.push(`Tags: ${tags}`);
|
|
100
108
|
}
|
|
109
|
+
if (createdNoteId) {
|
|
110
|
+
responseLines.push(`Note ID: ${createdNoteId}`);
|
|
111
|
+
}
|
|
101
112
|
const hasContent = title || text || tags;
|
|
102
113
|
const finalMessage = hasContent ? responseLines.join('\n') : 'Empty note created';
|
|
103
114
|
return createToolResponse(`${finalMessage}
|
|
@@ -201,7 +212,7 @@ Try different search criteria or check if notes exist in Bear Notes.`);
|
|
|
201
212
|
});
|
|
202
213
|
server.registerTool('bear-add-text', {
|
|
203
214
|
title: 'Add Text to Note',
|
|
204
|
-
description: '
|
|
215
|
+
description: 'Insert text at the beginning or end of a Bear note, or within a specific section identified by its header. Use bear-search-notes first to get the note ID. To insert without replacing existing text use this tool; to overwrite the direct content under a header use bear-replace-text.',
|
|
205
216
|
inputSchema: {
|
|
206
217
|
id: z
|
|
207
218
|
.string()
|
|
@@ -217,7 +228,7 @@ server.registerTool('bear-add-text', {
|
|
|
217
228
|
.string()
|
|
218
229
|
.trim()
|
|
219
230
|
.optional()
|
|
220
|
-
.describe('Optional section header to target (adds text within that section)'),
|
|
231
|
+
.describe('Optional section header to target (adds text within that section). Accepts any heading level, including the note title (H1).'),
|
|
221
232
|
position: z
|
|
222
233
|
.enum(['beginning', 'end'])
|
|
223
234
|
.optional()
|
|
@@ -235,7 +246,7 @@ server.registerTool('bear-add-text', {
|
|
|
235
246
|
});
|
|
236
247
|
server.registerTool('bear-replace-text', {
|
|
237
248
|
title: 'Replace Note Content',
|
|
238
|
-
description: 'Replace content in an existing Bear note — either the full body or a specific section. Requires content replacement to be enabled in extension settings. Use bear-search-notes first to get the note ID.',
|
|
249
|
+
description: 'Replace content in an existing Bear note — either the full body or a specific section. Requires content replacement to be enabled in extension settings. Use bear-search-notes first to get the note ID. To add text without replacing existing content use bear-add-text instead.',
|
|
239
250
|
inputSchema: {
|
|
240
251
|
id: z
|
|
241
252
|
.string()
|
|
@@ -249,12 +260,12 @@ server.registerTool('bear-replace-text', {
|
|
|
249
260
|
.string()
|
|
250
261
|
.trim()
|
|
251
262
|
.min(1, 'Text content is required')
|
|
252
|
-
.describe('Replacement text content'),
|
|
263
|
+
.describe('Replacement text content. When scope is "section", provide only the direct content for the targeted header — do not include markdown sub-headers (###). Replace sub-sections with separate calls targeting each sub-header.'),
|
|
253
264
|
header: z
|
|
254
265
|
.string()
|
|
255
266
|
.trim()
|
|
256
267
|
.optional()
|
|
257
|
-
.describe('Section header to target — required when scope is "section", forbidden when scope is "full-note-body"'),
|
|
268
|
+
.describe('Section header to target — required when scope is "section", forbidden when scope is "full-note-body". Accepts any heading level, including the note title (H1).'),
|
|
258
269
|
},
|
|
259
270
|
annotations: {
|
|
260
271
|
readOnlyHint: false,
|
package/dist/notes.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { setTimeout } from 'node:timers/promises';
|
|
1
2
|
import { DEFAULT_SEARCH_LIMIT } from './config.js';
|
|
2
3
|
import { convertCoreDataTimestamp, convertDateToCoreDataTimestamp, logAndThrow, logger, parseDateString, } from './utils.js';
|
|
3
|
-
import { openBearDatabase } from './database.js';
|
|
4
|
+
import { closeBearDatabase, openBearDatabase } from './database.js';
|
|
5
|
+
const POLL_INTERVAL_MS = 25;
|
|
6
|
+
const POLL_TIMEOUT_MS = 2_000;
|
|
7
|
+
// Safety window wider than POLL_TIMEOUT_MS to avoid matching a stale note with the same title
|
|
8
|
+
const CREATION_LOOKBACK_MS = 10_000;
|
|
4
9
|
// SQL equivalent of decodeTagName() in tags.ts — both MUST apply the same transformations
|
|
5
10
|
const DECODED_TAG_TITLE = "LOWER(TRIM(REPLACE(t.ZTITLE, '+', ' ')))";
|
|
6
11
|
/**
|
|
@@ -115,13 +120,7 @@ export function getNoteContent(identifier) {
|
|
|
115
120
|
logAndThrow(`Database error: Failed to retrieve note content: ${error instanceof Error ? error.message : String(error)}`);
|
|
116
121
|
}
|
|
117
122
|
finally {
|
|
118
|
-
|
|
119
|
-
db.close();
|
|
120
|
-
logger.debug('Database connection closed');
|
|
121
|
-
}
|
|
122
|
-
catch (closeError) {
|
|
123
|
-
logger.error(`Failed to close database connection: ${closeError}`);
|
|
124
|
-
}
|
|
123
|
+
closeBearDatabase(db);
|
|
125
124
|
}
|
|
126
125
|
return null;
|
|
127
126
|
}
|
|
@@ -257,13 +256,55 @@ export function searchNotes(searchTerm, tag, limit, dateFilter, pinned) {
|
|
|
257
256
|
logAndThrow(`SQLite search query failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
258
257
|
}
|
|
259
258
|
finally {
|
|
260
|
-
|
|
261
|
-
db.close();
|
|
262
|
-
logger.debug('Database connection closed');
|
|
263
|
-
}
|
|
264
|
-
catch (closeError) {
|
|
265
|
-
logger.error(`Failed to close database connection: ${closeError}`);
|
|
266
|
-
}
|
|
259
|
+
closeBearDatabase(db);
|
|
267
260
|
}
|
|
268
261
|
return { notes: [], totalCount: 0 };
|
|
269
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Polls Bear's SQLite database for the identifier of a recently created note.
|
|
265
|
+
* Designed for use after bear-create-note fires the URL API — the note creation already
|
|
266
|
+
* succeeded, so errors here degrade gracefully to null instead of throwing.
|
|
267
|
+
*
|
|
268
|
+
* @param title - Exact title to match (case-sensitive, as Bear stores it)
|
|
269
|
+
* @returns The created note's identifier, or null if not found within the timeout window
|
|
270
|
+
*/
|
|
271
|
+
export async function awaitNoteCreation(title) {
|
|
272
|
+
if (!title?.trim()) {
|
|
273
|
+
logger.debug('awaitNoteCreation: skipped — no title provided');
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
logger.debug(`awaitNoteCreation: polling for note "${title}"`);
|
|
277
|
+
const sinceTimestamp = convertDateToCoreDataTimestamp(new Date(Date.now() - CREATION_LOOKBACK_MS));
|
|
278
|
+
let db;
|
|
279
|
+
try {
|
|
280
|
+
db = openBearDatabase();
|
|
281
|
+
const stmt = db.prepare(`
|
|
282
|
+
SELECT ZUNIQUEIDENTIFIER as identifier
|
|
283
|
+
FROM ZSFNOTE
|
|
284
|
+
WHERE ZTITLE = ? AND ZCREATIONDATE >= ?
|
|
285
|
+
AND ZARCHIVED = 0 AND ZTRASHED = 0 AND ZENCRYPTED = 0
|
|
286
|
+
ORDER BY ZCREATIONDATE DESC LIMIT 1
|
|
287
|
+
`);
|
|
288
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
289
|
+
while (Date.now() < deadline) {
|
|
290
|
+
const row = stmt.get(title, sinceTimestamp);
|
|
291
|
+
if (row) {
|
|
292
|
+
logger.debug(`awaitNoteCreation: found note "${title}"`);
|
|
293
|
+
return row.identifier;
|
|
294
|
+
}
|
|
295
|
+
await setTimeout(POLL_INTERVAL_MS);
|
|
296
|
+
}
|
|
297
|
+
logger.info(`awaitNoteCreation: timed out waiting for note "${title}"`);
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
// Intentionally not using logAndThrow — the note was already created via URL API,
|
|
302
|
+
// failing to retrieve its ID should not turn a successful creation into an error
|
|
303
|
+
logger.error('awaitNoteCreation failed:', error);
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
if (db)
|
|
308
|
+
closeBearDatabase(db);
|
|
309
|
+
}
|
|
310
|
+
}
|
package/dist/tags.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { convertCoreDataTimestamp, logAndThrow, logger } from './utils.js';
|
|
2
|
-
import { openBearDatabase } from './database.js';
|
|
2
|
+
import { closeBearDatabase, openBearDatabase } from './database.js';
|
|
3
3
|
/**
|
|
4
4
|
* Decodes and normalizes Bear tag names.
|
|
5
5
|
* - Replaces '+' with spaces (Bear's URL encoding)
|
|
@@ -112,13 +112,7 @@ export function listTags() {
|
|
|
112
112
|
logAndThrow(`Database error: Failed to retrieve tags: ${error instanceof Error ? error.message : String(error)}`);
|
|
113
113
|
}
|
|
114
114
|
finally {
|
|
115
|
-
|
|
116
|
-
db.close();
|
|
117
|
-
logger.debug('Database connection closed');
|
|
118
|
-
}
|
|
119
|
-
catch (closeError) {
|
|
120
|
-
logger.error(`Failed to close database connection: ${closeError}`);
|
|
121
|
-
}
|
|
115
|
+
closeBearDatabase(db);
|
|
122
116
|
}
|
|
123
117
|
return { tags: [], totalCount: 0 };
|
|
124
118
|
}
|
|
@@ -167,13 +161,7 @@ export function findUntaggedNotes(limit = 50) {
|
|
|
167
161
|
logAndThrow(`Database error: Failed to find untagged notes: ${error instanceof Error ? error.message : String(error)}`);
|
|
168
162
|
}
|
|
169
163
|
finally {
|
|
170
|
-
|
|
171
|
-
db.close();
|
|
172
|
-
logger.debug('Database connection closed');
|
|
173
|
-
}
|
|
174
|
-
catch (closeError) {
|
|
175
|
-
logger.error(`Failed to close database connection: ${closeError}`);
|
|
176
|
-
}
|
|
164
|
+
closeBearDatabase(db);
|
|
177
165
|
}
|
|
178
166
|
return { notes: [], totalCount: 0 };
|
|
179
167
|
}
|