1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5// Package people provides MCP resources for filtered people lists.
6package people
7
8import (
9 "context"
10 "encoding/json"
11 "fmt"
12 "strings"
13 "time"
14
15 "git.secluded.site/go-lunatask"
16 "github.com/modelcontextprotocol/go-sdk/mcp"
17)
18
19// Resource URIs for people list filters.
20const (
21 ResourceURIAll = "lunatask://people"
22)
23
24// RelationshipTemplate is the URI template for relationship-filtered people resources.
25const RelationshipTemplate = "lunatask://people/{relationship}"
26
27// Resource descriptions.
28const (
29 AllDescription = `All people from relationship tracker.`
30
31 RelationshipDescription = `People filtered by relationship strength.
32{relationship} accepts: family, intimate-friends, close-friends, casual-friends,
33acquaintances, business-contacts, almost-strangers.`
34)
35
36// Handler handles people list resource requests.
37type Handler struct {
38 client *lunatask.Client
39}
40
41// NewHandler creates a new people resource handler.
42func NewHandler(accessToken string) *Handler {
43 return &Handler{
44 client: lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
45 }
46}
47
48// personSummary represents a person in list output.
49type personSummary struct {
50 DeepLink string `json:"deep_link"`
51 RelationshipStrength *string `json:"relationship_strength,omitempty"`
52 CreatedAt string `json:"created_at"`
53 UpdatedAt string `json:"updated_at"`
54}
55
56// HandleReadAll handles the all people resource.
57func (h *Handler) HandleReadAll(
58 ctx context.Context,
59 req *mcp.ReadResourceRequest,
60) (*mcp.ReadResourceResult, error) {
61 return h.handleFiltered(ctx, req, "")
62}
63
64// HandleReadByRelationship handles relationship-filtered people resources.
65func (h *Handler) HandleReadByRelationship(
66 ctx context.Context,
67 req *mcp.ReadResourceRequest,
68) (*mcp.ReadResourceResult, error) {
69 relationship := parseRelationshipURI(req.Params.URI)
70 if relationship == "" {
71 return nil, fmt.Errorf("invalid URI %q: %w", req.Params.URI, mcp.ResourceNotFoundError(req.Params.URI))
72 }
73
74 // Validate relationship strength
75 if _, err := lunatask.ParseRelationshipStrength(relationship); err != nil {
76 return nil, fmt.Errorf("invalid relationship %q: %w", relationship, mcp.ResourceNotFoundError(req.Params.URI))
77 }
78
79 return h.handleFiltered(ctx, req, relationship)
80}
81
82func (h *Handler) handleFiltered(
83 ctx context.Context,
84 req *mcp.ReadResourceRequest,
85 relationship string,
86) (*mcp.ReadResourceResult, error) {
87 people, err := h.client.ListPeople(ctx, nil)
88 if err != nil {
89 return nil, fmt.Errorf("fetching people: %w", err)
90 }
91
92 if relationship != "" {
93 people = filterByRelationship(people, relationship)
94 }
95
96 summaries := buildSummaries(people)
97
98 data, err := json.MarshalIndent(summaries, "", " ")
99 if err != nil {
100 return nil, fmt.Errorf("marshaling people: %w", err)
101 }
102
103 return &mcp.ReadResourceResult{
104 Contents: []*mcp.ResourceContents{{
105 URI: req.Params.URI,
106 MIMEType: "application/json",
107 Text: string(data),
108 }},
109 }, nil
110}
111
112func filterByRelationship(people []lunatask.Person, relationship string) []lunatask.Person {
113 result := make([]lunatask.Person, 0)
114
115 for _, person := range people {
116 if person.RelationshipStrength != nil && string(*person.RelationshipStrength) == relationship {
117 result = append(result, person)
118 }
119 }
120
121 return result
122}
123
124func buildSummaries(people []lunatask.Person) []personSummary {
125 summaries := make([]personSummary, 0, len(people))
126
127 for _, person := range people {
128 summary := personSummary{
129 CreatedAt: person.CreatedAt.Format(time.RFC3339),
130 UpdatedAt: person.UpdatedAt.Format(time.RFC3339),
131 }
132
133 summary.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourcePerson, person.ID)
134
135 if person.RelationshipStrength != nil {
136 s := string(*person.RelationshipStrength)
137 summary.RelationshipStrength = &s
138 }
139
140 summaries = append(summaries, summary)
141 }
142
143 return summaries
144}
145
146// parseRelationshipURI extracts relationship from the URI.
147// Example: lunatask://people/close-friends -> close-friends.
148func parseRelationshipURI(uri string) string {
149 const prefix = "lunatask://people/"
150
151 if !strings.HasPrefix(uri, prefix) {
152 return ""
153 }
154
155 return strings.TrimPrefix(uri, prefix)
156}