handler.go

  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}