handler.go

  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}