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 = `Adds a timeline note to a person's memory timeline in Lunatask.
22
23Required:
24- person_id: Person UUID or lunatask://person/... deep link
25
26Optional:
27- content: Markdown content describing the interaction
28- date: Date of interaction (YYYY-MM-DD or natural language, default: today)
29
30This is append-only — adds to the person's memory timeline.
31Great for tracking when you last interacted with someone.`
32
33// Input is the input schema for adding a timeline note.
34type Input struct {
35 PersonID string `json:"person_id" jsonschema:"required"`
36 Content *string `json:"content,omitempty"`
37 Date *string `json:"date,omitempty"`
38}
39
40// Output is the output schema for adding a timeline note.
41type Output struct {
42 Success bool `json:"success"`
43 PersonDeepLink string `json:"person_deep_link"`
44 NoteID string `json:"note_id"`
45}
46
47// Handler handles timeline note MCP tool requests.
48type Handler struct {
49 client *lunatask.Client
50}
51
52// NewHandler creates a new timeline handler.
53func NewHandler(accessToken string) *Handler {
54 return &Handler{
55 client: lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
56 }
57}
58
59// Handle adds a timeline note to a person.
60func (h *Handler) Handle(
61 ctx context.Context,
62 _ *mcp.CallToolRequest,
63 input Input,
64) (*mcp.CallToolResult, Output, error) {
65 _, personID, err := lunatask.ParseReference(input.PersonID)
66 if err != nil {
67 return shared.ErrorResult("invalid person_id: expected UUID or lunatask:// deep link"),
68 Output{}, nil
69 }
70
71 builder := h.client.NewTimelineNote(personID)
72
73 if input.Content != nil {
74 builder.WithContent(*input.Content)
75 }
76
77 if input.Date != nil {
78 date, err := dateutil.Parse(*input.Date)
79 if err != nil {
80 return shared.ErrorResult(err.Error()), Output{}, nil
81 }
82
83 builder.OnDate(date)
84 }
85
86 note, err := builder.Create(ctx)
87 if err != nil {
88 return shared.ErrorResult(err.Error()), Output{}, nil
89 }
90
91 personDeepLink, _ := lunatask.BuildDeepLink(lunatask.ResourcePerson, personID)
92
93 dateStr := "today"
94
95 if input.Date != nil {
96 date, _ := dateutil.Parse(*input.Date)
97 dateStr = date.Format("2006-01-02")
98 }
99
100 return &mcp.CallToolResult{
101 Content: []mcp.Content{&mcp.TextContent{
102 Text: "Timeline note added for " + personDeepLink + " on " + dateStr,
103 }},
104 }, Output{
105 Success: true,
106 PersonDeepLink: personDeepLink,
107 NoteID: note.ID,
108 }, nil
109}