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	}
 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}