index.js•14 kB
#!/usr/bin/env node
/**
 * Bear MCP Server
 *
 * A Model Context Protocol server for interacting with the Bear note-taking app's SQLite database.
 * This server provides read-only access to Bear notes and tags.
 */
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 { Command } from 'commander';
import { BearDB } from './bear-db.js';
import { formatBearDate } from './utils.js';
// Define the command-line interface
const program = new Command();
program
    .name('bear-mcp-server')
    .description('MCP server for read-only access to the Bear note-taking app database')
    .version('1.0.0')
    .option('--db-path <path>', 'Path to Bear SQLite database (defaults to standard location)')
    .parse(process.argv);
const options = program.opts();
// Create the Bear DB client
const bearDb = new BearDB({
    databasePath: options.dbPath,
});
// Define tool schemas
const openNoteSchema = {
    type: 'object',
    properties: {
        id: { type: 'string', description: 'Note unique identifier' },
        title: { type: 'string', description: 'Note title' },
        header: { type: 'string', description: 'An header inside the note' },
        exclude_trashed: { type: 'boolean', description: 'If true, exclude trashed notes' },
    },
};
const searchNotesSchema = {
    type: 'object',
    properties: {
        term: { type: 'string', description: 'String to search' },
        tag: { type: 'string', description: 'Tag to search into' },
        max_notes: { type: 'number', description: 'Maximum number of notes to return (default: 100)' },
    },
};
const getTagsSchema = {
    type: 'object',
    properties: {
        max_tags: { type: 'number', description: 'Maximum number of tags to return (default: 100)' },
    },
};
const openTagSchema = {
    type: 'object',
    properties: {
        name: { type: 'string', description: 'Tag name' },
        max_notes: { type: 'number', description: 'Maximum number of notes to return (default: 100)' },
    },
    required: ['name'],
};
/**
 * Bear MCP Server
 */
class BearMCPServer {
    constructor() {
        this.server = new Server({
            name: 'bear-mcp-server',
            version: '1.0.0',
        }, {
            capabilities: {
                tools: {},
            },
        });
        this.setupToolHandlers();
        // Error handling
        this.server.onerror = (error) => console.error('[MCP Error]', error);
        process.on('SIGINT', async () => {
            bearDb.close();
            await this.server.close();
            process.exit(0);
        });
    }
    /**
     * Sets up the tool handlers for the server
     */
    setupToolHandlers() {
        // List available tools - all read-only
        this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
            tools: [
                {
                    name: 'open_note',
                    description: 'Open a note identified by its title or id and return its content',
                    inputSchema: openNoteSchema,
                },
                {
                    name: 'search_notes',
                    description: 'Search for notes by term or tag',
                    inputSchema: searchNotesSchema,
                },
                {
                    name: 'get_tags',
                    description: 'Return all the tags currently in Bear',
                    inputSchema: getTagsSchema,
                },
                {
                    name: 'open_tag',
                    description: 'Show all the notes which have a selected tag',
                    inputSchema: openTagSchema,
                },
            ],
        }));
        // Handle tool calls
        this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
            const { name, arguments: args } = request.params;
            try {
                switch (name) {
                    case 'open_note':
                        return await this.handleOpenNote(args);
                    case 'search_notes':
                        return await this.handleSearchNotes(args);
                    case 'get_tags':
                        return await this.handleGetTags(args);
                    case 'open_tag':
                        return await this.handleOpenTag(args);
                    default:
                        throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
                }
            }
            catch (error) {
                console.error(`Error handling tool ${name}:`, error);
                return {
                    content: [
                        {
                            type: 'text',
                            text: `Error: ${error instanceof Error ? error.message : String(error)}`,
                        },
                    ],
                    isError: true,
                };
            }
        });
    }
    /**
     * Handles the open_note tool
     * @param args Tool arguments
     * @returns Tool result
     */
    async handleOpenNote(args) {
        try {
            const result = bearDb.getNoteByIdOrTitle(args);
            if (!result) {
                return {
                    content: [
                        {
                            type: 'text',
                            text: `Note not found.`,
                        },
                    ],
                    isError: true,
                };
            }
            // Format the creation and modification dates
            const creationDate = formatBearDate(result.creation_date);
            const modificationDate = formatBearDate(result.modification_date);
            // Add title, creation and modification dates to the content
            let content = `Title: ${result.title}\nCreated: ${creationDate}\nModified: ${modificationDate}\n\n${result.note}`;
            return {
                content: [
                    {
                        type: 'text',
                        text: content,
                    },
                ],
            };
        }
        catch (error) {
            console.error('Error handling open_note:', error);
            return {
                content: [
                    {
                        type: 'text',
                        text: `Error opening note: ${error instanceof Error ? error.message : String(error)}`,
                    },
                ],
                isError: true,
            };
        }
    }
    /**
     * Handles the search_notes tool
     * @param args Tool arguments
     * @returns Tool result
     */
    async handleSearchNotes(args) {
        try {
            const notes = bearDb.searchNotes(args);
            if (!notes || notes.length === 0) {
                return {
                    content: [
                        {
                            type: 'text',
                            text: `No notes found matching the search criteria.
              
Search parameters:
${args.term ? `- Search term: ${args.term}` : ''}
${args.tag ? `- Tag filter: ${args.tag}` : ''}`,
                        },
                    ],
                };
            }
            // Apply max_notes limit if specified (default: 100)
            const maxNotes = args.max_notes !== undefined ? args.max_notes : 100;
            const limitedNotes = notes.slice(0, maxNotes);
            const hasMoreNotes = notes.length > maxNotes;
            // Get the full content of each note
            const fullNotes = [];
            for (const note of limitedNotes) {
                try {
                    // Get the full content of each note
                    const noteResult = bearDb.getNoteByIdOrTitle({
                        id: note.identifier
                    });
                    if (noteResult) {
                        const tagsList = note.tags && Array.isArray(note.tags) ? `Tags: ${note.tags.join(', ')}` : '';
                        // Format the creation and modification dates
                        const creationDate = formatBearDate(noteResult.creation_date);
                        const modificationDate = formatBearDate(noteResult.modification_date);
                        // Add title, creation and modification dates to the content
                        let content = noteResult.note || 'Content not available';
                        content = `Title: ${noteResult.title}\nCreated: ${creationDate}\nModified: ${modificationDate}\n\n${content}`;
                        fullNotes.push({
                            title: note.title,
                            id: note.identifier,
                            tags: tagsList,
                            content: content
                        });
                    }
                }
                catch (e) {
                    console.error(`Error getting content for note ${note.identifier}:`, e);
                    fullNotes.push({
                        title: note.title,
                        id: note.identifier,
                        tags: note.tags && Array.isArray(note.tags) ? `Tags: ${note.tags.join(', ')}` : '',
                        content: 'Error retrieving content',
                        truncated: false
                    });
                }
            }
            // Format the search results with full content
            const formattedNotes = fullNotes.map(note => {
                return `## ${note.title} (ID: ${note.id})
${note.tags ? `${note.tags}\n` : ''}
${note.content}
---`;
            }).join('\n\n');
            return {
                content: [
                    {
                        type: 'text',
                        text: formattedNotes,
                    },
                ],
            };
        }
        catch (error) {
            console.error('Error handling search_notes:', error);
            return {
                content: [
                    {
                        type: 'text',
                        text: `Error performing search: ${error instanceof Error ? error.message : String(error)}`,
                    },
                ],
                isError: true,
            };
        }
    }
    /**
     * Handles the get_tags tool
     * @returns Tool result
     */
    async handleGetTags(args = {}) {
        try {
            const tags = bearDb.getTags();
            if (!tags || tags.length === 0) {
                return {
                    content: [
                        {
                            type: 'text',
                            text: `No tags found in Bear.`,
                        },
                    ],
                };
            }
            // Apply max_tags limit if specified (default: 100)
            const maxTags = args.max_tags !== undefined ? args.max_tags : 100;
            const limitedTags = tags.slice(0, maxTags);
            const hasMoreTags = tags.length > maxTags;
            // Format the tags
            const formattedTags = limitedTags.map(tag => `- ${tag.name}`).join('\n');
            return {
                content: [
                    {
                        type: 'text',
                        text: formattedTags,
                    },
                ],
            };
        }
        catch (error) {
            console.error('Error handling get_tags:', error);
            return {
                content: [
                    {
                        type: 'text',
                        text: `Error retrieving tags: ${error instanceof Error ? error.message : String(error)}`,
                    },
                ],
                isError: true,
            };
        }
    }
    /**
     * Handles the open_tag tool
     * @param args Tool arguments
     * @returns Tool result
     */
    async handleOpenTag(args) {
        try {
            const notes = bearDb.getNotesByTag(args.name);
            if (!notes || notes.length === 0) {
                return {
                    content: [
                        {
                            type: 'text',
                            text: `No notes found with tag: ${args.name}`,
                        },
                    ],
                };
            }
            // Apply max_notes limit if specified (default: 100)
            const maxNotes = args.max_notes !== undefined ? args.max_notes : 100;
            const limitedNotes = notes.slice(0, maxNotes);
            const hasMoreNotes = notes.length > maxNotes;
            // Format the notes
            const formattedNotes = limitedNotes.map(note => {
                const tagsList = note.tags && Array.isArray(note.tags) ? ` (Tags: ${note.tags.join(', ')})` : '';
                return `- ${note.title} (ID: ${note.identifier})${tagsList}`;
            }).join('\n');
            return {
                content: [
                    {
                        type: 'text',
                        text: formattedNotes,
                    },
                ],
            };
        }
        catch (error) {
            console.error('Error handling open_tag:', error);
            return {
                content: [
                    {
                        type: 'text',
                        text: `Error opening tag: ${error instanceof Error ? error.message : String(error)}`,
                    },
                ],
                isError: true,
            };
        }
    }
    /**
     * Runs the server
     */
    async run() {
        const transport = new StdioServerTransport();
        await this.server.connect(transport);
        console.error('Bear MCP server running on stdio');
    }
}
// Create and run the server
const server = new BearMCPServer();
server.run().catch(console.error);
//# sourceMappingURL=index.js.map