feat(mcp): add add_journal_entry tool

Amolith created

Assisted-by: Claude Sonnet 4 via Crush

Change summary

cmd/mcp/server.go                    |  34 +++++++--
internal/mcp/tools/journal/create.go | 102 ++++++++++++++++++++++++++++++
2 files changed, 128 insertions(+), 8 deletions(-)

Detailed changes

cmd/mcp/server.go 🔗

@@ -20,6 +20,7 @@ import (
 	taskrs "git.secluded.site/lune/internal/mcp/resources/task"
 	"git.secluded.site/lune/internal/mcp/shared"
 	"git.secluded.site/lune/internal/mcp/tools/habit"
+	"git.secluded.site/lune/internal/mcp/tools/journal"
 	"git.secluded.site/lune/internal/mcp/tools/task"
 	"git.secluded.site/lune/internal/mcp/tools/timestamp"
 	"github.com/modelcontextprotocol/go-sdk/mcp"
@@ -137,6 +138,31 @@ func registerTools(
 		}, tsHandler.Handle)
 	}
 
+	registerTaskTools(mcpServer, tools, accessToken, areaProviders)
+
+	if tools.TrackHabit {
+		habitHandler := habit.NewHandler(accessToken, habitProviders)
+		mcp.AddTool(mcpServer, &mcp.Tool{
+			Name:        habit.TrackToolName,
+			Description: habit.TrackToolDescription,
+		}, habitHandler.HandleTrack)
+	}
+
+	if tools.CreateJournal {
+		journalHandler := journal.NewHandler(accessToken)
+		mcp.AddTool(mcpServer, &mcp.Tool{
+			Name:        journal.CreateToolName,
+			Description: journal.CreateToolDescription,
+		}, journalHandler.HandleCreate)
+	}
+}
+
+func registerTaskTools(
+	mcpServer *mcp.Server,
+	tools *config.ToolsConfig,
+	accessToken string,
+	areaProviders []shared.AreaProvider,
+) {
 	taskHandler := task.NewHandler(accessToken, areaProviders)
 
 	if tools.CreateTask {
@@ -173,14 +199,6 @@ func registerTools(
 			Description: task.ShowToolDescription,
 		}, taskHandler.HandleShow)
 	}
-
-	if tools.TrackHabit {
-		habitHandler := habit.NewHandler(accessToken, habitProviders)
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        habit.TrackToolName,
-			Description: habit.TrackToolDescription,
-		}, habitHandler.HandleTrack)
-	}
 }
 
 func runStdio(mcpServer *mcp.Server) error {

internal/mcp/tools/journal/create.go 🔗

@@ -0,0 +1,102 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+// Package journal provides MCP tools for Lunatask journal operations.
+package journal
+
+import (
+	"context"
+
+	"git.secluded.site/go-lunatask"
+	"git.secluded.site/lune/internal/dateutil"
+	"git.secluded.site/lune/internal/mcp/shared"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
+)
+
+// CreateToolName is the name of the create journal entry tool.
+const CreateToolName = "add_journal_entry"
+
+// CreateToolDescription describes the create journal entry tool for LLMs.
+const CreateToolDescription = `Creates a journal entry in Lunatask for daily reflection.
+
+Optional:
+- content: Markdown content for the entry
+- name: Entry title (defaults to weekday name if omitted)
+- date: Entry date (YYYY-MM-DD or natural language, default: today)
+
+Entries are date-keyed. If no date is provided, uses today.
+Content supports Markdown formatting.
+
+Common uses:
+- End-of-day summaries
+- Reflection on completed work
+- Recording thoughts or learnings`
+
+// CreateInput is the input schema for creating a journal entry.
+type CreateInput struct {
+	Content *string `json:"content,omitempty"`
+	Name    *string `json:"name,omitempty"`
+	Date    *string `json:"date,omitempty"`
+}
+
+// CreateOutput is the output schema for creating a journal entry.
+type CreateOutput struct {
+	ID   string `json:"id"`
+	Date string `json:"date"`
+}
+
+// Handler handles journal-related MCP tool requests.
+type Handler struct {
+	client *lunatask.Client
+}
+
+// NewHandler creates a new journal handler.
+func NewHandler(accessToken string) *Handler {
+	return &Handler{
+		client: lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
+	}
+}
+
+// HandleCreate creates a new journal entry.
+func (h *Handler) HandleCreate(
+	ctx context.Context,
+	_ *mcp.CallToolRequest,
+	input CreateInput,
+) (*mcp.CallToolResult, CreateOutput, error) {
+	dateStr := ""
+	if input.Date != nil {
+		dateStr = *input.Date
+	}
+
+	date, err := dateutil.Parse(dateStr)
+	if err != nil {
+		return shared.ErrorResult(err.Error()), CreateOutput{}, nil
+	}
+
+	builder := h.client.NewJournalEntry(date)
+
+	if input.Content != nil {
+		builder.WithContent(*input.Content)
+	}
+
+	if input.Name != nil {
+		builder.WithName(*input.Name)
+	}
+
+	entry, err := builder.Create(ctx)
+	if err != nil {
+		return shared.ErrorResult(err.Error()), CreateOutput{}, nil
+	}
+
+	formattedDate := date.Format("2006-01-02")
+
+	return &mcp.CallToolResult{
+			Content: []mcp.Content{&mcp.TextContent{
+				Text: "Journal entry created for " + formattedDate,
+			}},
+		}, CreateOutput{
+			ID:   entry.ID,
+			Date: formattedDate,
+		}, nil
+}