bear-notes-mcp 2.6.0 → 2.8.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 +2 -0
- package/dist/bear-urls.js +11 -1
- package/dist/config.js +1 -1
- package/dist/database.js +13 -0
- package/dist/main.js +90 -5
- package/dist/notes.js +62 -17
- package/dist/tags.js +12 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,8 @@ Search, read, create, and update your Bear Notes from any AI assistant.
|
|
|
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
|
|
28
28
|
- **`bear-archive-note`** - Archive a Bear note to remove it from active lists without deleting it
|
|
29
|
+
- **`bear-rename-tag`** - Rename a tag across all notes in your Bear library
|
|
30
|
+
- **`bear-delete-tag`** - Delete a tag from all notes in your Bear library without affecting the notes
|
|
29
31
|
<!-- TOOLS:END -->
|
|
30
32
|
|
|
31
33
|
**Requirements**: Node.js 24.13.0+
|
package/dist/bear-urls.js
CHANGED
|
@@ -17,7 +17,17 @@ export function buildBearUrl(action, params = {}) {
|
|
|
17
17
|
const baseUrl = `${BEAR_URL_SCHEME}${action.trim()}`;
|
|
18
18
|
const urlParams = new URLSearchParams();
|
|
19
19
|
// Add provided parameters with proper encoding
|
|
20
|
-
const stringParams = [
|
|
20
|
+
const stringParams = [
|
|
21
|
+
'title',
|
|
22
|
+
'text',
|
|
23
|
+
'tags',
|
|
24
|
+
'id',
|
|
25
|
+
'header',
|
|
26
|
+
'file',
|
|
27
|
+
'filename',
|
|
28
|
+
'name',
|
|
29
|
+
'new_name',
|
|
30
|
+
];
|
|
21
31
|
for (const key of stringParams) {
|
|
22
32
|
const value = params[key];
|
|
23
33
|
if (value !== undefined && value.trim()) {
|
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,7 +5,7 @@ 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({
|
|
@@ -65,7 +65,7 @@ ${noteText}`);
|
|
|
65
65
|
});
|
|
66
66
|
server.registerTool('bear-create-note', {
|
|
67
67
|
title: 'Create New Note',
|
|
68
|
-
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.',
|
|
69
69
|
inputSchema: {
|
|
70
70
|
title: z
|
|
71
71
|
.string()
|
|
@@ -98,16 +98,17 @@ server.registerTool('bear-create-note', {
|
|
|
98
98
|
: { text, tags };
|
|
99
99
|
const url = buildBearUrl('create', { title, text: createText, tags: createTags });
|
|
100
100
|
await executeBearXCallbackApi(url);
|
|
101
|
+
const createdNoteId = title ? await awaitNoteCreation(title) : undefined;
|
|
101
102
|
const responseLines = ['Bear note created successfully!', ''];
|
|
102
103
|
if (title) {
|
|
103
104
|
responseLines.push(`Title: "${title}"`);
|
|
104
105
|
}
|
|
105
|
-
if (text) {
|
|
106
|
-
responseLines.push(`Content: ${text.length} characters`);
|
|
107
|
-
}
|
|
108
106
|
if (tags) {
|
|
109
107
|
responseLines.push(`Tags: ${tags}`);
|
|
110
108
|
}
|
|
109
|
+
if (createdNoteId) {
|
|
110
|
+
responseLines.push(`Note ID: ${createdNoteId}`);
|
|
111
|
+
}
|
|
111
112
|
const hasContent = title || text || tags;
|
|
112
113
|
const finalMessage = hasContent ? responseLines.join('\n') : 'Empty note created';
|
|
113
114
|
return createToolResponse(`${finalMessage}
|
|
@@ -545,6 +546,90 @@ The note has been moved to Bear's archive.`);
|
|
|
545
546
|
throw error;
|
|
546
547
|
}
|
|
547
548
|
});
|
|
549
|
+
server.registerTool('bear-rename-tag', {
|
|
550
|
+
title: 'Rename Tag',
|
|
551
|
+
description: 'Rename a tag across all notes in your Bear library. Useful for reorganizing tag taxonomy, fixing typos, or restructuring tag hierarchies. Use bear-list-tags first to see existing tags.',
|
|
552
|
+
inputSchema: {
|
|
553
|
+
name: z
|
|
554
|
+
.string()
|
|
555
|
+
.trim()
|
|
556
|
+
.transform((v) => v.replace(/^#/, ''))
|
|
557
|
+
.pipe(z.string().min(1, 'Tag name is required'))
|
|
558
|
+
.describe('Current tag name to rename (without # symbol)'),
|
|
559
|
+
new_name: z
|
|
560
|
+
.string()
|
|
561
|
+
.trim()
|
|
562
|
+
.transform((v) => v.replace(/^#/, ''))
|
|
563
|
+
.pipe(z.string().min(1, 'New tag name is required'))
|
|
564
|
+
.describe('New tag name (without # symbol). Use slashes for hierarchy, e.g., "archive/old-project"'),
|
|
565
|
+
},
|
|
566
|
+
annotations: {
|
|
567
|
+
readOnlyHint: false,
|
|
568
|
+
destructiveHint: true,
|
|
569
|
+
idempotentHint: false,
|
|
570
|
+
openWorldHint: true,
|
|
571
|
+
},
|
|
572
|
+
}, async ({ name, new_name }) => {
|
|
573
|
+
logger.info(`bear-rename-tag called with name: "${name}", new_name: "${new_name}"`);
|
|
574
|
+
try {
|
|
575
|
+
const url = buildBearUrl('rename-tag', {
|
|
576
|
+
name,
|
|
577
|
+
new_name,
|
|
578
|
+
open_note: 'no',
|
|
579
|
+
new_window: 'no',
|
|
580
|
+
show_window: 'no',
|
|
581
|
+
});
|
|
582
|
+
await executeBearXCallbackApi(url);
|
|
583
|
+
return createToolResponse(`Tag renamed successfully!
|
|
584
|
+
|
|
585
|
+
From: #${name}
|
|
586
|
+
To: #${new_name}
|
|
587
|
+
|
|
588
|
+
The tag has been renamed across all notes in your Bear library.`);
|
|
589
|
+
}
|
|
590
|
+
catch (error) {
|
|
591
|
+
logger.error('bear-rename-tag failed:', error);
|
|
592
|
+
throw error;
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
server.registerTool('bear-delete-tag', {
|
|
596
|
+
title: 'Delete Tag',
|
|
597
|
+
description: 'Delete a tag from all notes in your Bear library. Removes the tag but preserves the notes themselves. Use bear-list-tags first to see existing tags.',
|
|
598
|
+
inputSchema: {
|
|
599
|
+
name: z
|
|
600
|
+
.string()
|
|
601
|
+
.trim()
|
|
602
|
+
.transform((v) => v.replace(/^#/, ''))
|
|
603
|
+
.pipe(z.string().min(1, 'Tag name is required'))
|
|
604
|
+
.describe('Tag name to delete (without # symbol)'),
|
|
605
|
+
},
|
|
606
|
+
annotations: {
|
|
607
|
+
readOnlyHint: false,
|
|
608
|
+
destructiveHint: true,
|
|
609
|
+
idempotentHint: false,
|
|
610
|
+
openWorldHint: true,
|
|
611
|
+
},
|
|
612
|
+
}, async ({ name }) => {
|
|
613
|
+
logger.info(`bear-delete-tag called with name: "${name}"`);
|
|
614
|
+
try {
|
|
615
|
+
const url = buildBearUrl('delete-tag', {
|
|
616
|
+
name,
|
|
617
|
+
open_note: 'no',
|
|
618
|
+
new_window: 'no',
|
|
619
|
+
show_window: 'no',
|
|
620
|
+
});
|
|
621
|
+
await executeBearXCallbackApi(url);
|
|
622
|
+
return createToolResponse(`Tag deleted successfully!
|
|
623
|
+
|
|
624
|
+
Tag: #${name}
|
|
625
|
+
|
|
626
|
+
The tag has been removed from all notes. The notes themselves are not affected.`);
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
logger.error('bear-delete-tag failed:', error);
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
});
|
|
548
633
|
async function main() {
|
|
549
634
|
logger.info(`Bear Notes MCP Server initializing... Version: ${APP_VERSION}`);
|
|
550
635
|
logger.debug(`Debug logs enabled: ${logger.debug.enabled}`);
|
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
|
/**
|
|
@@ -92,8 +97,12 @@ export function getNoteContent(identifier) {
|
|
|
92
97
|
const rowData = row;
|
|
93
98
|
const filename = rowData.filename;
|
|
94
99
|
const fileContent = rowData.fileContent;
|
|
95
|
-
if (filename
|
|
96
|
-
|
|
100
|
+
if (filename) {
|
|
101
|
+
const trimmed = fileContent?.trim();
|
|
102
|
+
const content = trimmed
|
|
103
|
+
? trimmed
|
|
104
|
+
: '*[File content not available — Bear has not extracted text from this file type]*';
|
|
105
|
+
fileContents.push(`##${filename}\n\n${content}`);
|
|
97
106
|
}
|
|
98
107
|
}
|
|
99
108
|
// Always append file content section, even if empty, to show structure
|
|
@@ -115,13 +124,7 @@ export function getNoteContent(identifier) {
|
|
|
115
124
|
logAndThrow(`Database error: Failed to retrieve note content: ${error instanceof Error ? error.message : String(error)}`);
|
|
116
125
|
}
|
|
117
126
|
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
|
-
}
|
|
127
|
+
closeBearDatabase(db);
|
|
125
128
|
}
|
|
126
129
|
return null;
|
|
127
130
|
}
|
|
@@ -257,13 +260,55 @@ export function searchNotes(searchTerm, tag, limit, dateFilter, pinned) {
|
|
|
257
260
|
logAndThrow(`SQLite search query failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
258
261
|
}
|
|
259
262
|
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
|
-
}
|
|
263
|
+
closeBearDatabase(db);
|
|
267
264
|
}
|
|
268
265
|
return { notes: [], totalCount: 0 };
|
|
269
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Polls Bear's SQLite database for the identifier of a recently created note.
|
|
269
|
+
* Designed for use after bear-create-note fires the URL API — the note creation already
|
|
270
|
+
* succeeded, so errors here degrade gracefully to null instead of throwing.
|
|
271
|
+
*
|
|
272
|
+
* @param title - Exact title to match (case-sensitive, as Bear stores it)
|
|
273
|
+
* @returns The created note's identifier, or null if not found within the timeout window
|
|
274
|
+
*/
|
|
275
|
+
export async function awaitNoteCreation(title) {
|
|
276
|
+
if (!title?.trim()) {
|
|
277
|
+
logger.debug('awaitNoteCreation: skipped — no title provided');
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
logger.debug(`awaitNoteCreation: polling for note "${title}"`);
|
|
281
|
+
const sinceTimestamp = convertDateToCoreDataTimestamp(new Date(Date.now() - CREATION_LOOKBACK_MS));
|
|
282
|
+
let db;
|
|
283
|
+
try {
|
|
284
|
+
db = openBearDatabase();
|
|
285
|
+
const stmt = db.prepare(`
|
|
286
|
+
SELECT ZUNIQUEIDENTIFIER as identifier
|
|
287
|
+
FROM ZSFNOTE
|
|
288
|
+
WHERE ZTITLE = ? AND ZCREATIONDATE >= ?
|
|
289
|
+
AND ZARCHIVED = 0 AND ZTRASHED = 0 AND ZENCRYPTED = 0
|
|
290
|
+
ORDER BY ZCREATIONDATE DESC LIMIT 1
|
|
291
|
+
`);
|
|
292
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
293
|
+
while (Date.now() < deadline) {
|
|
294
|
+
const row = stmt.get(title, sinceTimestamp);
|
|
295
|
+
if (row) {
|
|
296
|
+
logger.debug(`awaitNoteCreation: found note "${title}"`);
|
|
297
|
+
return row.identifier;
|
|
298
|
+
}
|
|
299
|
+
await setTimeout(POLL_INTERVAL_MS);
|
|
300
|
+
}
|
|
301
|
+
logger.info(`awaitNoteCreation: timed out waiting for note "${title}"`);
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
// Intentionally not using logAndThrow — the note was already created via URL API,
|
|
306
|
+
// failing to retrieve its ID should not turn a successful creation into an error
|
|
307
|
+
logger.error('awaitNoteCreation failed:', error);
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
if (db)
|
|
312
|
+
closeBearDatabase(db);
|
|
313
|
+
}
|
|
314
|
+
}
|
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)
|
|
@@ -21,14 +21,12 @@ function getTagDisplayName(fullPath) {
|
|
|
21
21
|
/**
|
|
22
22
|
* Builds a hierarchical tree from a flat list of tags.
|
|
23
23
|
* Tags with paths like "career/content" become children of "career".
|
|
24
|
-
*
|
|
24
|
+
* Caller is responsible for excluding zero-count tags before passing data here.
|
|
25
25
|
*/
|
|
26
26
|
function buildTagHierarchy(flatTags) {
|
|
27
|
-
// Filter out tags with no notes (hidden in Bear UI)
|
|
28
|
-
const activeTags = flatTags.filter((t) => t.noteCount > 0);
|
|
29
27
|
const tagMap = new Map();
|
|
30
28
|
// Two-pass approach: first create nodes, then link parent-child relationships
|
|
31
|
-
for (const tag of
|
|
29
|
+
for (const tag of flatTags) {
|
|
32
30
|
tagMap.set(tag.name, {
|
|
33
31
|
name: tag.name,
|
|
34
32
|
displayName: tag.displayName,
|
|
@@ -38,7 +36,7 @@ function buildTagHierarchy(flatTags) {
|
|
|
38
36
|
}
|
|
39
37
|
const roots = [];
|
|
40
38
|
// Build parent-child relationships
|
|
41
|
-
for (const tag of
|
|
39
|
+
for (const tag of flatTags) {
|
|
42
40
|
const tagNode = tagMap.get(tag.name);
|
|
43
41
|
if (tag.isRoot) {
|
|
44
42
|
roots.push(tagNode);
|
|
@@ -82,10 +80,15 @@ export function listTags() {
|
|
|
82
80
|
const query = `
|
|
83
81
|
SELECT t.ZTITLE as name,
|
|
84
82
|
t.ZISROOT as isRoot,
|
|
85
|
-
COUNT(
|
|
83
|
+
COUNT(note.Z_PK) as noteCount
|
|
86
84
|
FROM ZSFNOTETAG t
|
|
87
85
|
LEFT JOIN Z_5TAGS nt ON nt.Z_13TAGS = t.Z_PK
|
|
86
|
+
LEFT JOIN ZSFNOTE note ON note.Z_PK = nt.Z_5NOTES
|
|
87
|
+
AND note.ZTRASHED = 0
|
|
88
|
+
AND note.ZARCHIVED = 0
|
|
89
|
+
AND note.ZENCRYPTED = 0
|
|
88
90
|
GROUP BY t.Z_PK
|
|
91
|
+
HAVING noteCount > 0
|
|
89
92
|
ORDER BY t.ZTITLE
|
|
90
93
|
`;
|
|
91
94
|
const stmt = db.prepare(query);
|
|
@@ -112,13 +115,7 @@ export function listTags() {
|
|
|
112
115
|
logAndThrow(`Database error: Failed to retrieve tags: ${error instanceof Error ? error.message : String(error)}`);
|
|
113
116
|
}
|
|
114
117
|
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
|
-
}
|
|
118
|
+
closeBearDatabase(db);
|
|
122
119
|
}
|
|
123
120
|
return { tags: [], totalCount: 0 };
|
|
124
121
|
}
|
|
@@ -167,13 +164,7 @@ export function findUntaggedNotes(limit = 50) {
|
|
|
167
164
|
logAndThrow(`Database error: Failed to find untagged notes: ${error instanceof Error ? error.message : String(error)}`);
|
|
168
165
|
}
|
|
169
166
|
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
|
-
}
|
|
167
|
+
closeBearDatabase(db);
|
|
177
168
|
}
|
|
178
169
|
return { notes: [], totalCount: 0 };
|
|
179
170
|
}
|