1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5// Package person provides the MCP resource handler for individual Lunatask people.
6package person
7
8import (
9 "context"
10 "encoding/json"
11 "fmt"
12 "time"
13
14 "git.secluded.site/go-lunatask"
15 "github.com/modelcontextprotocol/go-sdk/mcp"
16)
17
18// ResourceTemplate is the URI template for person resources.
19const ResourceTemplate = "lunatask://person/{id}"
20
21// ResourceDescription describes the person resource for LLMs.
22const ResourceDescription = `Reads metadata for a specific Lunatask person by ID or deep link.
23
24Due to end-to-end encryption, person name is not available.
25Returns metadata including relationship strength and sources.`
26
27// sourceInfo represents a source reference in the response.
28type sourceInfo struct {
29 Source string `json:"source"`
30 SourceID string `json:"source_id"`
31}
32
33// personInfo represents person metadata in the resource response.
34type personInfo struct {
35 DeepLink string `json:"deep_link"`
36 RelationshipStrength *string `json:"relationship_strength,omitempty"`
37 Sources []sourceInfo `json:"sources,omitempty"`
38 CreatedAt string `json:"created_at"`
39 UpdatedAt string `json:"updated_at"`
40}
41
42// Handler handles person resource requests.
43type Handler struct {
44 client *lunatask.Client
45}
46
47// NewHandler creates a new person resource handler.
48func NewHandler(accessToken string) *Handler {
49 return &Handler{
50 client: lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
51 }
52}
53
54// HandleRead returns metadata for a specific person.
55func (h *Handler) HandleRead(
56 ctx context.Context,
57 req *mcp.ReadResourceRequest,
58) (*mcp.ReadResourceResult, error) {
59 _, id, err := lunatask.ParseReference(req.Params.URI)
60 if err != nil {
61 return nil, fmt.Errorf("invalid URI %q: %w", req.Params.URI, mcp.ResourceNotFoundError(req.Params.URI))
62 }
63
64 person, err := h.client.GetPerson(ctx, id)
65 if err != nil {
66 return nil, fmt.Errorf("fetching person: %w", err)
67 }
68
69 info := buildPersonInfo(person)
70
71 data, err := json.MarshalIndent(info, "", " ")
72 if err != nil {
73 return nil, fmt.Errorf("marshaling person: %w", err)
74 }
75
76 return &mcp.ReadResourceResult{
77 Contents: []*mcp.ResourceContents{{
78 URI: req.Params.URI,
79 MIMEType: "application/json",
80 Text: string(data),
81 }},
82 }, nil
83}
84
85func buildPersonInfo(person *lunatask.Person) personInfo {
86 info := personInfo{
87 CreatedAt: person.CreatedAt.Format(time.RFC3339),
88 UpdatedAt: person.UpdatedAt.Format(time.RFC3339),
89 }
90
91 info.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourcePerson, person.ID)
92
93 if person.RelationshipStrength != nil {
94 s := string(*person.RelationshipStrength)
95 info.RelationshipStrength = &s
96 }
97
98 if len(person.Sources) > 0 {
99 info.Sources = make([]sourceInfo, 0, len(person.Sources))
100 for _, src := range person.Sources {
101 info.Sources = append(info.Sources, sourceInfo{
102 Source: src.Source,
103 SourceID: src.SourceID,
104 })
105 }
106 }
107
108 return info
109}