1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package person
6
7import (
8 "context"
9 "fmt"
10 "strings"
11
12 "git.secluded.site/go-lunatask"
13 "git.secluded.site/lune/internal/mcp/shared"
14 "github.com/modelcontextprotocol/go-sdk/mcp"
15)
16
17// ListToolName is the name of the list people tool.
18const ListToolName = "list_people"
19
20// ListToolDescription describes the list people tool for LLMs.
21const ListToolDescription = `Lists people from Lunatask's relationship tracker.
22
23Optional:
24- source: Filter by source identifier
25- source_id: Filter by source-specific ID
26
27Returns person metadata (IDs, relationship strength, created dates).
28Use lunatask://person/{id} resource for full person details.
29
30Note: Due to end-to-end encryption, names are not available
31in the list — only metadata is returned.`
32
33// ListInput is the input schema for listing people.
34type ListInput struct {
35 Source *string `json:"source,omitempty"`
36 SourceID *string `json:"source_id,omitempty"`
37}
38
39// ListPersonItem represents a person in the list output.
40type ListPersonItem struct {
41 DeepLink string `json:"deep_link"`
42 RelationshipStrength *string `json:"relationship_strength,omitempty"`
43 CreatedAt string `json:"created_at"`
44}
45
46// ListOutput is the output schema for listing people.
47type ListOutput struct {
48 People []ListPersonItem `json:"people"`
49 Count int `json:"count"`
50}
51
52// HandleList lists people.
53func (h *Handler) HandleList(
54 ctx context.Context,
55 _ *mcp.CallToolRequest,
56 input ListInput,
57) (*mcp.CallToolResult, ListOutput, error) {
58 opts := buildListOptions(input)
59
60 people, err := h.client.ListPeople(ctx, opts)
61 if err != nil {
62 return shared.ErrorResult(err.Error()), ListOutput{}, nil
63 }
64
65 items := make([]ListPersonItem, 0, len(people))
66
67 for _, person := range people {
68 deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourcePerson, person.ID)
69
70 item := ListPersonItem{
71 DeepLink: deepLink,
72 CreatedAt: person.CreatedAt.Format("2006-01-02"),
73 }
74
75 if person.RelationshipStrength != nil {
76 rel := string(*person.RelationshipStrength)
77 item.RelationshipStrength = &rel
78 }
79
80 items = append(items, item)
81 }
82
83 output := ListOutput{
84 People: items,
85 Count: len(items),
86 }
87
88 text := formatListText(items)
89
90 return &mcp.CallToolResult{
91 Content: []mcp.Content{&mcp.TextContent{Text: text}},
92 }, output, nil
93}
94
95func buildListOptions(input ListInput) *lunatask.ListPeopleOptions {
96 if input.Source == nil && input.SourceID == nil {
97 return nil
98 }
99
100 opts := &lunatask.ListPeopleOptions{}
101
102 if input.Source != nil {
103 opts.Source = input.Source
104 }
105
106 if input.SourceID != nil {
107 opts.SourceID = input.SourceID
108 }
109
110 return opts
111}
112
113func formatListText(items []ListPersonItem) string {
114 if len(items) == 0 {
115 return "No people found"
116 }
117
118 var text strings.Builder
119
120 text.WriteString(fmt.Sprintf("Found %d person(s):\n", len(items)))
121
122 for _, item := range items {
123 text.WriteString("- ")
124 text.WriteString(item.DeepLink)
125
126 if item.RelationshipStrength != nil {
127 text.WriteString(" (")
128 text.WriteString(*item.RelationshipStrength)
129 text.WriteString(")")
130 }
131
132 text.WriteString("\n")
133 }
134
135 text.WriteString("\nUse lunatask://person/{id} resource for full details.")
136
137 return text.String()
138}