list.go

  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}