handler.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5// Package timeline provides the MCP tool for adding timeline notes to people.
  6package timeline
  7
  8import (
  9	"context"
 10
 11	"git.secluded.site/go-lunatask"
 12	"git.secluded.site/lune/internal/dateutil"
 13	"git.secluded.site/lune/internal/mcp/shared"
 14	"github.com/modelcontextprotocol/go-sdk/mcp"
 15)
 16
 17// ToolName is the name of the add timeline note tool.
 18const ToolName = "add_timeline_note"
 19
 20// ToolDescription describes the add timeline note tool for LLMs.
 21const ToolDescription = `Add a note to a person's memory timeline.
 22
 23Append-only—each call creates a new timeline entry. Great for tracking
 24interactions, meetings, or memorable moments with someone.`
 25
 26// ToolAnnotations returns hints about tool behavior.
 27func ToolAnnotations() *mcp.ToolAnnotations {
 28	return &mcp.ToolAnnotations{
 29		DestructiveHint: ptr(false),
 30		OpenWorldHint:   ptr(true),
 31		Title:           "Add timeline note",
 32	}
 33}
 34
 35func ptr[T any](v T) *T { return &v }
 36
 37// Input is the input schema for adding a timeline note.
 38type Input struct {
 39	PersonID string  `json:"person_id"         jsonschema:"Person UUID or lunatask:// deep link"`
 40	Content  *string `json:"content,omitempty" jsonschema:"Markdown content describing the interaction"`
 41	Date     *string `json:"date,omitempty"    jsonschema:"Date of interaction (strtotime syntax, default: today)"`
 42}
 43
 44// Output is the output schema for adding a timeline note.
 45type Output struct {
 46	Success        bool   `json:"success"`
 47	PersonDeepLink string `json:"person_deep_link"`
 48	NoteID         string `json:"note_id"`
 49}
 50
 51// Handler handles timeline note MCP tool requests.
 52type Handler struct {
 53	client *lunatask.Client
 54}
 55
 56// NewHandler creates a new timeline handler.
 57func NewHandler(accessToken string) *Handler {
 58	return &Handler{
 59		client: lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
 60	}
 61}
 62
 63// Handle adds a timeline note to a person.
 64func (h *Handler) Handle(
 65	ctx context.Context,
 66	_ *mcp.CallToolRequest,
 67	input Input,
 68) (*mcp.CallToolResult, Output, error) {
 69	_, personID, err := lunatask.ParseReference(input.PersonID)
 70	if err != nil {
 71		return shared.ErrorResult("invalid person_id: expected UUID or lunatask:// deep link"),
 72			Output{}, nil
 73	}
 74
 75	builder := h.client.NewTimelineNote(personID)
 76
 77	if input.Content != nil {
 78		builder.WithContent(*input.Content)
 79	}
 80
 81	if input.Date != nil {
 82		date, err := dateutil.Parse(*input.Date)
 83		if err != nil {
 84			return shared.ErrorResult(err.Error()), Output{}, nil
 85		}
 86
 87		builder.OnDate(date)
 88	}
 89
 90	note, err := builder.Create(ctx)
 91	if err != nil {
 92		return shared.ErrorResult(err.Error()), Output{}, nil
 93	}
 94
 95	personDeepLink, _ := lunatask.BuildDeepLink(lunatask.ResourcePerson, personID)
 96
 97	dateStr := "today"
 98
 99	if input.Date != nil {
100		date, _ := dateutil.Parse(*input.Date)
101		dateStr = date.Format("2006-01-02")
102	}
103
104	return &mcp.CallToolResult{
105			Content: []mcp.Content{&mcp.TextContent{
106				Text: "Timeline note added for " + personDeepLink + " on " + dateStr,
107			}},
108		}, Output{
109			Success:        true,
110			PersonDeepLink: personDeepLink,
111			NoteID:         note.ID,
112		}, nil
113}