Skip to main content
Glama

Bear App MCP Server

by bigjeager
index.ts34.6 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js"; import { exec } from "child_process"; import { promisify } from "util"; import { createServer } from "http"; import { URL } from "url"; const execAsync = promisify(exec); interface BearNote { title?: string; identifier?: string; tags?: string[]; modificationDate?: string; creationDate?: string; pin?: boolean; note?: string; is_trashed?: boolean; } interface BearResponse { notes?: BearNote[]; tags?: Array<{ name: string }>; note?: string; identifier?: string; title?: string; is_trashed?: boolean; modificationDate?: string; creationDate?: string; } class BearMCPServer { private server: Server; private token?: string; constructor() { this.server = new Server( { name: "bear-app-server", version: "1.0.0", } ); this.setupToolHandlers(); } private async executeURL(url: string): Promise<string> { try { const command = `open "${url}"`; const { stdout, stderr } = await execAsync(command); if (stderr) { throw new Error(`Bear command failed: ${stderr}`); } return stdout; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to execute Bear URL: ${error instanceof Error ? error.message : String(error)}` ); } } private buildBearURL(action: string, params: Record<string, string | boolean> = {}): string { const baseURL = `bear://x-callback-url/${action}`; const queryParts: string[] = []; for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null) { const encodedKey = encodeURIComponent(key); const encodedValue = encodeURIComponent(String(value)); queryParts.push(`${encodedKey}=${encodedValue}`); } } const queryString = queryParts.join('&'); return queryString ? `${baseURL}?${queryString}` : baseURL; } private async executeWithCallback(action: string, params: Record<string, string | boolean> = {}): Promise<any> { return new Promise((resolve, reject) => { // Create a temporary HTTP server to receive the callback const server = createServer((req, res) => { if (req.url) { const url = new URL(req.url, 'http://localhost'); const searchParams = url.searchParams; try { // Extract all callback parameters dynamically const callbackData: Record<string, any> = {}; for (const [key, value] of searchParams.entries()) { if (key === 'notes' || key === 'tags') { // Handle JSON array parameters try { callbackData[key] = JSON.parse(value); } catch { // If JSON parsing fails, treat as regular string callbackData[key] = value; } } else if (key === 'tags' && !callbackData[key]) { // Handle comma-separated tags for open-note callbackData[key] = value ? value.split(',').filter(Boolean) : []; } else if (key === 'is_trashed' || key === 'pin') { // Handle boolean parameters callbackData[key] = value === 'yes'; } else { // Handle regular string parameters callbackData[key] = value; } } // Send HTML response that immediately closes the browser window res.writeHead(200, { 'Content-Type': 'text/html', 'X-Frame-Options': 'DENY', 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0' }); const closeHtml = ` <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="refresh" content="0; url=about:blank"> <title>Bear MCP Callback</title> <style>body { display: none; }</style> </head> <body> <script> // Multiple methods to close the window immediately try { window.close(); window.open('', '_self', ''); window.close(); setTimeout(() => window.close(), 1); setTimeout(() => history.back(), 10); } catch(e) {} </script> </body> </html> `; res.end(closeHtml); server.close(); resolve(callbackData); } catch (error) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error parsing callback data'); server.close(); reject(new Error(`Failed to parse callback data: ${error instanceof Error ? error.message : String(error)}`)); } } }); // Start server on a random available port server.listen(0, () => { const address = server.address(); if (address && typeof address === 'object') { const callbackUrl = `http://localhost:${address.port}/callback`; // Add x-success callback URL to params params['x-success'] = callbackUrl; // Build and execute Bear URL const bearUrl = this.buildBearURL(action, params); this.executeURL(bearUrl).catch(reject); // Set timeout to avoid hanging forever setTimeout(() => { server.close(); reject(new Error('Callback timeout')); }, 10000); } else { reject(new Error('Failed to start callback server')); } }); }); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "bear_open_note", description: "Open a note in Bear by ID or title", inputSchema: { type: "object", properties: { id: { type: "string", description: "Note unique identifier" }, title: { type: "string", description: "Note title" }, header: { type: "string", description: "Header inside the note" }, exclude_trashed: { type: "boolean", description: "Exclude trashed notes" }, new_window: { type: "boolean", description: "Open in external window (macOS only)" }, edit: { type: "boolean", description: "Place cursor in note editor" }, selected: { type: "string", description: "Selected text in note" }, pin: { type: "boolean", description: "Pin note to top of list" }, float: { type: "boolean", description: "Float note window" }, show_window: { type: "boolean", description: "Show Bear window" }, open_note: { type: "boolean", description: "Open note after command" }, search: { type: "string", description: "Search term within note" } } } }, { name: "bear_create_note", description: "Create a new note in Bear", inputSchema: { type: "object", properties: { title: { type: "string", description: "Note title" }, text: { type: "string", description: "Note content" }, tags: { type: "string", description: "Comma-separated list of tags" }, pin: { type: "boolean", description: "Pin note to top of list" }, timestamp: { type: "boolean", description: "Prepend current date and time" }, clipboard: { type: "boolean", description: "Get text from clipboard" }, file: { type: "string", description: "File path to add to note" }, filename: { type: "string", description: "Custom filename for attached file" }, open_note: { type: "boolean", description: "Open note after creation" }, new_window: { type: "boolean", description: "Open in new window" }, float: { type: "boolean", description: "Float note window" }, show_window: { type: "boolean", description: "Show Bear window" }, edit: { type: "boolean", description: "Place cursor in note editor" }, type: { type: "string", description: "Note type" }, url: { type: "string", description: "URL to include in note" } } } }, { name: "bear_add_text", description: "Add text to an existing note", inputSchema: { type: "object", properties: { id: { type: "string", description: "Note unique identifier" }, title: { type: "string", description: "Note title" }, text: { type: "string", description: "Text to add" }, mode: { type: "string", enum: ["append", "prepend", "replace_all", "replace"], description: "How to add the text" }, new_line: { type: "boolean", description: "Force text on new line when appending" }, header: { type: "string", description: "Add text to specific header" }, selected: { type: "string", description: "Selected text in note" }, clipboard: { type: "boolean", description: "Get text from clipboard" }, exclude_trashed: { type: "boolean", description: "Exclude trashed notes" }, open_note: { type: "boolean", description: "Open note after adding text" }, new_window: { type: "boolean", description: "Open in new window" }, show_window: { type: "boolean", description: "Show Bear window" }, edit: { type: "boolean", description: "Place cursor in note editor" }, timestamp: { type: "boolean", description: "Prepend current date and time" } }, required: ["text"] } }, { name: "bear_add_file", description: "Add a file to an existing note", inputSchema: { type: "object", properties: { id: { type: "string", description: "Note unique identifier" }, title: { type: "string", description: "Note title" }, selected: { type: "string", description: "Selected text in note" }, file: { type: "string", description: "File path to add" }, header: { type: "string", description: "Add file to specific header" }, filename: { type: "string", description: "Custom filename for the file" }, mode: { type: "string", enum: ["append", "prepend", "replace_all", "replace"], description: "How to add the file" }, open_note: { type: "boolean", description: "Open note after adding file" }, new_window: { type: "boolean", description: "Open in new window" }, show_window: { type: "boolean", description: "Show Bear window" }, edit: { type: "boolean", description: "Place cursor in note editor" } }, required: ["file"] } }, { name: "bear_search", description: "Search for notes in Bear", inputSchema: { type: "object", properties: { term: { type: "string", description: "Search term" }, tag: { type: "string", description: "Tag to search within" }, token: { type: "string", description: "Bear API token" }, show_window: { type: "boolean", description: "Show Bear window" } } } }, { name: "bear_get_tags", description: "Get all tags from Bear", inputSchema: { type: "object", properties: { token: { type: "string", description: "Bear API token (required)", required: true } }, required: ["token"] } }, { name: "bear_open_tag", description: "Open notes with specific tag(s)", inputSchema: { type: "object", properties: { name: { type: "string", description: "Tag name or comma-separated list of tags" }, token: { type: "string", description: "Bear API token" }, show_window: { type: "boolean", description: "Show Bear window" } }, required: ["name"] } }, { name: "bear_trash_note", description: "Move a note to trash", inputSchema: { type: "object", properties: { id: { type: "string", description: "Note unique identifier" }, search: { type: "string", description: "Search term to find notes to trash" }, show_window: { type: "boolean", description: "Show Bear window" } } } }, { name: "bear_archive_note", description: "Archive a note", inputSchema: { type: "object", properties: { id: { type: "string", description: "Note unique identifier" }, search: { type: "string", description: "Search term to find notes to archive" }, show_window: { type: "boolean", description: "Show Bear window" } } } }, { name: "bear_get_untagged", description: "Get untagged notes", inputSchema: { type: "object", properties: { search: { type: "string", description: "Search term" }, token: { type: "string", description: "Bear API token" }, show_window: { type: "boolean", description: "Show Bear window" } } } }, { name: "bear_get_todo", description: "Get todo notes", inputSchema: { type: "object", properties: { search: { type: "string", description: "Search term" }, token: { type: "string", description: "Bear API token" }, show_window: { type: "boolean", description: "Show Bear window" } } } }, { name: "bear_get_today", description: "Get today's notes", inputSchema: { type: "object", properties: { search: { type: "string", description: "Search term" }, token: { type: "string", description: "Bear API token" }, show_window: { type: "boolean", description: "Show Bear window" } } } }, { name: "bear_get_locked", description: "Get locked (encrypted) notes", inputSchema: { type: "object", properties: { search: { type: "string", description: "Search term" }, show_window: { type: "boolean", description: "Show Bear window" } } } }, { name: "bear_grab_url", description: "Create a note from web page content", inputSchema: { type: "object", properties: { url: { type: "string", description: "URL to grab content from" }, tags: { type: "string", description: "Comma-separated list of tags" }, pin: { type: "boolean", description: "Pin note to top of list" }, wait: { type: "boolean", description: "Wait for content to load" } }, required: ["url"] } }, { name: "bear_rename_tag", description: "Rename an existing tag", inputSchema: { type: "object", properties: { name: { type: "string", description: "Current tag name" }, new_name: { type: "string", description: "New tag name" }, show_window: { type: "boolean", description: "Show Bear window" } }, required: ["name", "new_name"] } }, { name: "bear_delete_tag", description: "Delete an existing tag", inputSchema: { type: "object", properties: { name: { type: "string", description: "Tag name to delete" }, show_window: { type: "boolean", description: "Show Bear window" } }, required: ["name"] } } ] }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "bear_open_note": return await this.openNote(args); case "bear_create_note": return await this.createNote(args); case "bear_add_text": return await this.addText(args); case "bear_add_file": return await this.addFile(args); case "bear_search": return await this.search(args); case "bear_get_tags": return await this.getTags(args); case "bear_open_tag": return await this.openTag(args); case "bear_trash_note": return await this.trashNote(args); case "bear_archive_note": return await this.archiveNote(args); case "bear_get_untagged": return await this.getUntagged(args); case "bear_get_todo": return await this.getTodo(args); case "bear_get_today": return await this.getToday(args); case "bear_get_locked": return await this.getLocked(args); case "bear_grab_url": return await this.grabUrl(args); case "bear_rename_tag": return await this.renameTag(args); case "bear_delete_tag": return await this.deleteTag(args); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } } catch (error) { throw new McpError( ErrorCode.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}` ); } }); } private async openNote(args: any) { const params: Record<string, string | boolean> = {}; if (args.id) params.id = args.id; if (args.title) params.title = args.title; if (args.header) params.header = args.header; if (args.exclude_trashed) params.exclude_trashed = "yes"; if (args.new_window) params.new_window = "yes"; if (args.edit) params.edit = "yes"; if (args.selected) params.selected = args.selected; if (args.pin) params.pin = "yes"; if (args.float) params.float = "yes"; if (args.show_window) params.show_window = "yes"; if (args.open_note) params.open_note = "yes"; if (args.search) params.search = args.search; // Set up a temporary HTTP server to capture the x-success callback const noteData = await this.executeWithCallback("open-note", params); return { content: [ { type: "text", text: JSON.stringify(noteData, null, 2) } ] }; } private async createNote(args: any) { const params: Record<string, string | boolean> = {}; if (args.title) params.title = args.title; if (args.text) params.text = args.text; if (args.tags) params.tags = args.tags; if (args.pin) params.pin = "yes"; if (args.timestamp) params.timestamp = "yes"; if (args.clipboard) params.clipboard = "yes"; if (args.file) params.file = args.file; if (args.filename) params.filename = args.filename; if (args.open_note) params.open_note = "yes"; if (args.new_window) params.new_window = "yes"; if (args.float) params.float = "yes"; if (args.show_window) params.show_window = "yes"; if (args.edit) params.edit = "yes"; if (args.type) params.type = args.type; if (args.url) params.url = args.url; const noteData = await this.executeWithCallback("create", params); return { content: [ { type: "text", text: JSON.stringify({ message: `Created new note in Bear${args.title ? ` with title: ${args.title}` : ""}`, note: noteData }, null, 2) } ] }; } private async addText(args: any) { const params: Record<string, string | boolean> = {}; if (args.id) params.id = args.id; if (args.title) params.title = args.title; if (args.text) params.text = args.text; if (args.mode) params.mode = args.mode; if (args.new_line) params.new_line = "yes"; if (args.header) params.header = args.header; if (args.selected) params.selected = args.selected; if (args.clipboard) params.clipboard = "yes"; if (args.exclude_trashed) params.exclude_trashed = "yes"; if (args.open_note) params.open_note = "yes"; if (args.new_window) params.new_window = "yes"; if (args.show_window) params.show_window = "yes"; if (args.edit) params.edit = "yes"; if (args.timestamp) params.timestamp = "yes"; const url = this.buildBearURL("add-text", params); await this.executeURL(url); return { content: [ { type: "text", text: `Added text to note in Bear${args.mode ? ` using mode: ${args.mode}` : ""}` } ] }; } private async search(args: any) { const params: Record<string, string | boolean> = {}; if (args.term) params.term = args.term; if (args.tag) params.tag = args.tag; if (args.token) params.token = args.token; if (args.show_window) params.show_window = "yes"; const searchData = await this.executeWithCallback("search", params); return { content: [ { type: "text", text: JSON.stringify({ message: `Searched Bear for: ${args.term || "all notes"}${args.tag ? ` in tag: ${args.tag}` : ""}`, results: searchData }, null, 2) } ] }; } private async getTags(args: any) { if (!args.token) { throw new McpError(ErrorCode.InvalidParams, "Token is required for getTags"); } const params = { token: args.token }; const tagsData = await this.executeWithCallback("tags", params); return { content: [ { type: "text", text: JSON.stringify({ message: "Retrieved all tags from Bear", tags: tagsData }, null, 2) } ] }; } private async openTag(args: any) { const params: Record<string, string | boolean> = { name: args.name }; if (args.token) params.token = args.token; if (args.show_window) params.show_window = "yes"; const tagData = await this.executeWithCallback("open-tag", params); return { content: [ { type: "text", text: JSON.stringify({ message: `Opened notes with tag: ${args.name}`, notes: tagData }, null, 2) } ] }; } private async trashNote(args: any) { const params: Record<string, string | boolean> = {}; if (args.id) params.id = args.id; if (args.search) params.search = args.search; if (args.show_window) params.show_window = "yes"; const url = this.buildBearURL("trash", params); await this.executeURL(url); return { content: [ { type: "text", text: `Moved note(s) to trash${args.id ? ` with ID: ${args.id}` : args.search ? ` matching: ${args.search}` : ""}` } ] }; } private async archiveNote(args: any) { const params: Record<string, string | boolean> = {}; if (args.id) params.id = args.id; if (args.search) params.search = args.search; if (args.show_window) params.show_window = "yes"; const url = this.buildBearURL("archive", params); await this.executeURL(url); return { content: [ { type: "text", text: `Archived note(s)${args.id ? ` with ID: ${args.id}` : args.search ? ` matching: ${args.search}` : ""}` } ] }; } private async getUntagged(args: any) { const params: Record<string, string | boolean> = {}; if (args.search) params.search = args.search; if (args.token) params.token = args.token; if (args.show_window) params.show_window = "yes"; const untaggedData = await this.executeWithCallback("untagged", params); return { content: [ { type: "text", text: JSON.stringify({ message: `Retrieved untagged notes${args.search ? ` matching: ${args.search}` : ""}`, notes: untaggedData }, null, 2) } ] }; } private async getTodo(args: any) { const params: Record<string, string | boolean> = {}; if (args.search) params.search = args.search; if (args.token) params.token = args.token; if (args.show_window) params.show_window = "yes"; const todoData = await this.executeWithCallback("todo", params); return { content: [ { type: "text", text: JSON.stringify({ message: `Retrieved todo notes${args.search ? ` matching: ${args.search}` : ""}`, notes: todoData }, null, 2) } ] }; } private async getToday(args: any) { const params: Record<string, string | boolean> = {}; if (args.search) params.search = args.search; if (args.token) params.token = args.token; if (args.show_window) params.show_window = "yes"; const todayData = await this.executeWithCallback("today", params); return { content: [ { type: "text", text: JSON.stringify({ message: `Retrieved today's notes${args.search ? ` matching: ${args.search}` : ""}`, notes: todayData }, null, 2) } ] }; } private async getLocked(args: any) { const params: Record<string, string | boolean> = {}; if (args.search) params.search = args.search; if (args.show_window) params.show_window = "yes"; const url = this.buildBearURL("locked", params); await this.executeURL(url); return { content: [ { type: "text", text: `Retrieved locked notes${args.search ? ` matching: ${args.search}` : ""}` } ] }; } private async addFile(args: any) { const params: Record<string, string | boolean> = {}; if (args.id) params.id = args.id; if (args.title) params.title = args.title; if (args.selected) params.selected = args.selected; if (args.file) params.file = args.file; if (args.header) params.header = args.header; if (args.filename) params.filename = args.filename; if (args.mode) params.mode = args.mode; if (args.open_note) params.open_note = "yes"; if (args.new_window) params.new_window = "yes"; if (args.show_window) params.show_window = "yes"; if (args.edit) params.edit = "yes"; const url = this.buildBearURL("add-file", params); await this.executeURL(url); return { content: [ { type: "text", text: `Added file to note in Bear${args.filename ? ` with filename: ${args.filename}` : ""}` } ] }; } private async grabUrl(args: any) { const params: Record<string, string | boolean> = { url: args.url }; if (args.tags) params.tags = args.tags; if (args.pin) params.pin = "yes"; if (args.wait) params.wait = "yes"; const grabData = await this.executeWithCallback("grab-url", params); return { content: [ { type: "text", text: JSON.stringify({ message: `Created note from URL: ${args.url}`, note: grabData }, null, 2) } ] }; } private async renameTag(args: any) { const params: Record<string, string | boolean> = { name: args.name, new_name: args.new_name }; if (args.show_window) params.show_window = "yes"; const url = this.buildBearURL("rename-tag", params); await this.executeURL(url); return { content: [ { type: "text", text: `Renamed tag from "${args.name}" to "${args.new_name}"` } ] }; } private async deleteTag(args: any) { const params: Record<string, string | boolean> = { name: args.name }; if (args.show_window) params.show_window = "yes"; const url = this.buildBearURL("delete-tag", params); await this.executeURL(url); return { content: [ { type: "text", text: `Deleted tag: ${args.name}` } ] }; } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("Bear MCP server running on stdio"); } } const server = new BearMCPServer(); server.run().catch(console.error);

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bigjeager/bear-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server