Skip to main content
Glama
index.ts7.4 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { JrnlExecutor } from "./utils/jrnlExecutor.js"; import { searchEntries } from "./handlers/entryHandlers.js"; import { listTags, analyzeTagCooccurrence } from "./handlers/tagHandlers.js"; import { getStatistics } from "./handlers/statisticsHandlers.js"; import { listJournals, setJournal, getCurrentJournal, } from "./handlers/journalHandlers.js"; const server = new Server( { name: "jrnl-mcp", version: "1.0.0", }, { capabilities: { tools: {}, }, }, ); const executor = new JrnlExecutor(); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "search_entries", description: "Search and filter journal entries", inputSchema: { type: "object", properties: { from: { type: "string", description: 'Start date (e.g., "yesterday", "2024-01-01")', }, to: { type: "string", description: "End date" }, tags: { type: "array", items: { type: "string" }, description: "Tags to filter by", }, contains: { type: "string", description: "Text to search for" }, limit: { type: "number", description: "Maximum number of entries" }, starred: { type: "boolean", description: "Only show starred entries", }, journal: { type: "string", description: "Journal name (uses current/default if not specified)", }, }, }, }, { name: "list_tags", description: "List all tags with their usage counts", inputSchema: { type: "object", properties: { journal: { type: "string", description: "Journal name (uses current/default if not specified)", }, }, }, }, { name: "analyze_tag_cooccurrence", description: "Analyze which tags frequently appear together", inputSchema: { type: "object", properties: { tags: { type: "array", items: { type: "string" }, description: "Tags to analyze for co-occurrence", minItems: 2, }, journal: { type: "string", description: "Journal name (uses current/default if not specified)", }, }, required: ["tags"], }, }, { name: "get_statistics", description: "Get journal statistics and analytics", inputSchema: { type: "object", properties: { journal: { type: "string", description: "Journal name (uses current/default if not specified)", }, timeGrouping: { type: "string", enum: ["day", "week", "month", "year"], description: "Group statistics by time period", }, includeTopTags: { type: "boolean", description: "Include top tags in statistics", }, }, }, }, { name: "list_journals", description: "List all available journals", inputSchema: { type: "object", properties: {}, }, }, { name: "set_journal", description: "Set the active journal for subsequent operations", inputSchema: { type: "object", properties: { journalName: { type: "string", description: "Name of the journal to set as active", }, }, required: ["journalName"], }, }, ], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { const journal = (typeof args?.journal === "string" ? args.journal : undefined) || getCurrentJournal(); switch (name) { case "search_entries": return { content: [ { type: "text", text: JSON.stringify( await searchEntries({ ...args, journal } as any, executor), null, 2, ), }, ], }; case "list_tags": return { content: [ { type: "text", text: JSON.stringify(await listTags(journal, executor), null, 2), }, ], }; case "analyze_tag_cooccurrence": return { content: [ { type: "text", text: JSON.stringify( await analyzeTagCooccurrence( Array.isArray(args?.tags) ? args.tags : [], journal, executor, ), null, 2, ), }, ], }; case "get_statistics": return { content: [ { type: "text", text: JSON.stringify( await getStatistics( journal, typeof args?.timeGrouping === "string" ? args.timeGrouping : undefined, typeof args?.includeTopTags === "boolean" ? args.includeTopTags : true, executor, ), null, 2, ), }, ], }; case "list_journals": return { content: [ { type: "text", text: JSON.stringify(await listJournals(executor), null, 2), }, ], }; case "set_journal": return { content: [ { type: "text", text: JSON.stringify( await setJournal( typeof args?.journalName === "string" ? args.journalName : "", ), null, 2, ), }, ], }; default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { // Log errors to stderr for debugging process.stderr.write( `[${new Date().toISOString()}] Error in tool ${name}: ${error instanceof Error ? error.message : String(error)}\n`, ); return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }); async function main() { try { const transport = new StdioServerTransport(); await server.connect(transport); // Server started - logging to stderr for debugging process.stderr.write( `[${new Date().toISOString()}] jrnl MCP server started\n`, ); } catch (error) { process.stderr.write( `[${new Date().toISOString()}] Failed to start MCP server: ${error}\n`, ); process.exit(1); } } main().catch((error) => { process.stderr.write( `[${new Date().toISOString()}] Server error: ${error}\n`, ); process.exit(1); });

Latest Blog Posts

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/yostos/jrnl-mcp'

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