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