jrnl-mcp
Version:
Model Context Protocol server for jrnl CLI journal application
228 lines (227 loc) • 8.77 kB
JavaScript
#!/usr/bin/env node
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
const jrnlExecutor_js_1 = require("./utils/jrnlExecutor.js");
const entryHandlers_js_1 = require("./handlers/entryHandlers.js");
const tagHandlers_js_1 = require("./handlers/tagHandlers.js");
const statisticsHandlers_js_1 = require("./handlers/statisticsHandlers.js");
const journalHandlers_js_1 = require("./handlers/journalHandlers.js");
const server = new index_js_1.Server({
name: "jrnl-mcp",
version: "1.0.0",
}, {
capabilities: {
tools: {},
},
});
const executor = new jrnlExecutor_js_1.JrnlExecutor();
server.setRequestHandler(types_js_1.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(types_js_1.CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const journal = (typeof args?.journal === "string" ? args.journal : undefined) ||
(0, journalHandlers_js_1.getCurrentJournal)();
switch (name) {
case "search_entries":
return {
content: [
{
type: "text",
text: JSON.stringify(await (0, entryHandlers_js_1.searchEntries)({ ...args, journal }, executor), null, 2),
},
],
};
case "list_tags":
return {
content: [
{
type: "text",
text: JSON.stringify(await (0, tagHandlers_js_1.listTags)(journal, executor), null, 2),
},
],
};
case "analyze_tag_cooccurrence":
return {
content: [
{
type: "text",
text: JSON.stringify(await (0, tagHandlers_js_1.analyzeTagCooccurrence)(Array.isArray(args?.tags) ? args.tags : [], journal, executor), null, 2),
},
],
};
case "get_statistics":
return {
content: [
{
type: "text",
text: JSON.stringify(await (0, statisticsHandlers_js_1.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 (0, journalHandlers_js_1.listJournals)(executor), null, 2),
},
],
};
case "set_journal":
return {
content: [
{
type: "text",
text: JSON.stringify(await (0, journalHandlers_js_1.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 stdio_js_1.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);
});