bear-notes-mcp 2.7.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 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 = ['title', 'text', 'tags', 'id', 'header', 'file', 'filename'];
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
@@ -1,4 +1,4 @@
1
- export const APP_VERSION = '2.7.0';
1
+ export const APP_VERSION = '2.8.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;
package/dist/main.js CHANGED
@@ -546,6 +546,90 @@ The note has been moved to Bear's archive.`);
546
546
  throw error;
547
547
  }
548
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
+ });
549
633
  async function main() {
550
634
  logger.info(`Bear Notes MCP Server initializing... Version: ${APP_VERSION}`);
551
635
  logger.debug(`Debug logs enabled: ${logger.debug.enabled}`);
package/dist/notes.js CHANGED
@@ -97,8 +97,12 @@ export function getNoteContent(identifier) {
97
97
  const rowData = row;
98
98
  const filename = rowData.filename;
99
99
  const fileContent = rowData.fileContent;
100
- if (filename && fileContent && fileContent.trim()) {
101
- fileContents.push(`##${filename}\n\n${fileContent.trim()}`);
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}`);
102
106
  }
103
107
  }
104
108
  // Always append file content section, even if empty, to show structure
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
- * Tags with 0 notes are excluded (matches Bear UI behavior).
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 activeTags) {
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 activeTags) {
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(nt.Z_5NOTES) as noteCount
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bear-notes-mcp",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "Bear Notes MCP server with TypeScript and native SQLite",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",