bear-notes-mcp 2.7.0 → 2.8.1
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/main.js +105 -5
- package/dist/notes.js +12 -15
- package/dist/tags.js +9 -6
- 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/main.js
CHANGED
|
@@ -52,11 +52,27 @@ Use bear-search-notes to find the correct note identifier.`);
|
|
|
52
52
|
`ID: ${noteWithContent.identifier}`,
|
|
53
53
|
];
|
|
54
54
|
const noteText = noteWithContent.text || '*This note appears to be empty.*';
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
const annotations = { audience: ['user', 'assistant'] };
|
|
56
|
+
// Body and file metadata are separate content blocks so the synthetic
|
|
57
|
+
// file section can never leak back during write operations (#86)
|
|
58
|
+
const content = [
|
|
59
|
+
{
|
|
60
|
+
type: 'text',
|
|
61
|
+
text: `${noteInfo.join('\n')}\n\n---\n\n${noteText}`,
|
|
62
|
+
annotations,
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
if (noteWithContent.files?.length) {
|
|
66
|
+
const fileEntries = noteWithContent.files
|
|
67
|
+
.map((f) => `## ${f.filename}\n\n${f.content}`)
|
|
68
|
+
.join('\n\n---\n\n');
|
|
69
|
+
content.push({
|
|
70
|
+
type: 'text',
|
|
71
|
+
text: `# Attached Files\n\n${fileEntries}`,
|
|
72
|
+
annotations,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return { content };
|
|
60
76
|
}
|
|
61
77
|
catch (error) {
|
|
62
78
|
logger.error('bear-open-note failed:', error);
|
|
@@ -546,6 +562,90 @@ The note has been moved to Bear's archive.`);
|
|
|
546
562
|
throw error;
|
|
547
563
|
}
|
|
548
564
|
});
|
|
565
|
+
server.registerTool('bear-rename-tag', {
|
|
566
|
+
title: 'Rename Tag',
|
|
567
|
+
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.',
|
|
568
|
+
inputSchema: {
|
|
569
|
+
name: z
|
|
570
|
+
.string()
|
|
571
|
+
.trim()
|
|
572
|
+
.transform((v) => v.replace(/^#/, ''))
|
|
573
|
+
.pipe(z.string().min(1, 'Tag name is required'))
|
|
574
|
+
.describe('Current tag name to rename (without # symbol)'),
|
|
575
|
+
new_name: z
|
|
576
|
+
.string()
|
|
577
|
+
.trim()
|
|
578
|
+
.transform((v) => v.replace(/^#/, ''))
|
|
579
|
+
.pipe(z.string().min(1, 'New tag name is required'))
|
|
580
|
+
.describe('New tag name (without # symbol). Use slashes for hierarchy, e.g., "archive/old-project"'),
|
|
581
|
+
},
|
|
582
|
+
annotations: {
|
|
583
|
+
readOnlyHint: false,
|
|
584
|
+
destructiveHint: true,
|
|
585
|
+
idempotentHint: false,
|
|
586
|
+
openWorldHint: true,
|
|
587
|
+
},
|
|
588
|
+
}, async ({ name, new_name }) => {
|
|
589
|
+
logger.info(`bear-rename-tag called with name: "${name}", new_name: "${new_name}"`);
|
|
590
|
+
try {
|
|
591
|
+
const url = buildBearUrl('rename-tag', {
|
|
592
|
+
name,
|
|
593
|
+
new_name,
|
|
594
|
+
open_note: 'no',
|
|
595
|
+
new_window: 'no',
|
|
596
|
+
show_window: 'no',
|
|
597
|
+
});
|
|
598
|
+
await executeBearXCallbackApi(url);
|
|
599
|
+
return createToolResponse(`Tag renamed successfully!
|
|
600
|
+
|
|
601
|
+
From: #${name}
|
|
602
|
+
To: #${new_name}
|
|
603
|
+
|
|
604
|
+
The tag has been renamed across all notes in your Bear library.`);
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
logger.error('bear-rename-tag failed:', error);
|
|
608
|
+
throw error;
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
server.registerTool('bear-delete-tag', {
|
|
612
|
+
title: 'Delete Tag',
|
|
613
|
+
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.',
|
|
614
|
+
inputSchema: {
|
|
615
|
+
name: z
|
|
616
|
+
.string()
|
|
617
|
+
.trim()
|
|
618
|
+
.transform((v) => v.replace(/^#/, ''))
|
|
619
|
+
.pipe(z.string().min(1, 'Tag name is required'))
|
|
620
|
+
.describe('Tag name to delete (without # symbol)'),
|
|
621
|
+
},
|
|
622
|
+
annotations: {
|
|
623
|
+
readOnlyHint: false,
|
|
624
|
+
destructiveHint: true,
|
|
625
|
+
idempotentHint: false,
|
|
626
|
+
openWorldHint: true,
|
|
627
|
+
},
|
|
628
|
+
}, async ({ name }) => {
|
|
629
|
+
logger.info(`bear-delete-tag called with name: "${name}"`);
|
|
630
|
+
try {
|
|
631
|
+
const url = buildBearUrl('delete-tag', {
|
|
632
|
+
name,
|
|
633
|
+
open_note: 'no',
|
|
634
|
+
new_window: 'no',
|
|
635
|
+
show_window: 'no',
|
|
636
|
+
});
|
|
637
|
+
await executeBearXCallbackApi(url);
|
|
638
|
+
return createToolResponse(`Tag deleted successfully!
|
|
639
|
+
|
|
640
|
+
Tag: #${name}
|
|
641
|
+
|
|
642
|
+
The tag has been removed from all notes. The notes themselves are not affected.`);
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
logger.error('bear-delete-tag failed:', error);
|
|
646
|
+
throw error;
|
|
647
|
+
}
|
|
648
|
+
});
|
|
549
649
|
async function main() {
|
|
550
650
|
logger.info(`Bear Notes MCP Server initializing... Version: ${APP_VERSION}`);
|
|
551
651
|
logger.debug(`Debug logs enabled: ${logger.debug.enabled}`);
|
package/dist/notes.js
CHANGED
|
@@ -91,28 +91,25 @@ export function getNoteContent(identifier) {
|
|
|
91
91
|
// Process multiple rows (note + files) into single note object
|
|
92
92
|
const firstRow = rows[0];
|
|
93
93
|
const formattedNote = formatBearNote(firstRow);
|
|
94
|
-
// Collect file content
|
|
95
|
-
|
|
94
|
+
// Collect file content into a structured array — kept separate from note text
|
|
95
|
+
// to prevent the synthetic file section from leaking into write operations (#86)
|
|
96
|
+
const files = [];
|
|
96
97
|
for (const row of rows) {
|
|
97
98
|
const rowData = row;
|
|
98
99
|
const filename = rowData.filename;
|
|
99
100
|
const fileContent = rowData.fileContent;
|
|
100
|
-
if (filename
|
|
101
|
-
|
|
101
|
+
if (filename) {
|
|
102
|
+
const trimmed = fileContent?.trim();
|
|
103
|
+
const content = trimmed
|
|
104
|
+
? trimmed
|
|
105
|
+
: '*[File content not available — Bear has not extracted text from this file type]*';
|
|
106
|
+
files.push({ filename, content });
|
|
102
107
|
}
|
|
103
108
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const filesSectionHeader = '\n\n---\n\n#Attached Files\n\n';
|
|
107
|
-
if (fileContents.length > 0) {
|
|
108
|
-
const fileSection = `${filesSectionHeader}${fileContents.join('\n\n---\n\n')}`;
|
|
109
|
-
formattedNote.text = originalText + fileSection;
|
|
109
|
+
if (files.length > 0) {
|
|
110
|
+
formattedNote.files = files;
|
|
110
111
|
}
|
|
111
|
-
|
|
112
|
-
// Add a note that no files are attached for clarity
|
|
113
|
-
formattedNote.text = originalText + `${filesSectionHeader}*No files attached to this note.*`;
|
|
114
|
-
}
|
|
115
|
-
logger.info(`Retrieved note content with ${fileContents.length} attached files for: ${formattedNote.title}`);
|
|
112
|
+
logger.info(`Retrieved note content with ${files.length} attached files for: ${formattedNote.title}`);
|
|
116
113
|
return formattedNote;
|
|
117
114
|
}
|
|
118
115
|
catch (error) {
|
package/dist/tags.js
CHANGED
|
@@ -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);
|