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}