bear-notes-mcp 2.6.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/dist/config.js CHANGED
@@ -1,4 +1,4 @@
1
- export const APP_VERSION = '2.6.0';
1
+ export const APP_VERSION = '2.7.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/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}
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
- try {
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
- try {
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
- try {
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
- try {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bear-notes-mcp",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "Bear Notes MCP server with TypeScript and native SQLite",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",