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 "time"
12
13 "git.secluded.site/go-lunatask"
14 "git.secluded.site/lune/internal/mcp/shared"
15 "github.com/modelcontextprotocol/go-sdk/mcp"
16)
17
18// ShowToolName is the name of the show person tool.
19const ShowToolName = "show_person"
20
21// ShowToolDescription describes the show person tool for LLMs.
22const ShowToolDescription = `Shows metadata for a specific person from Lunatask.
23
24Required:
25- id: Person UUID or lunatask:// deep link
26
27Note: Due to end-to-end encryption, person name is not available.
28Only metadata (relationship strength, sources) is returned.`
29
30// ShowInput is the input schema for showing a person.
31type ShowInput struct {
32 ID string `json:"id" jsonschema:"required"`
33}
34
35// ShowSource represents a source reference in the output.
36type ShowSource struct {
37 Source string `json:"source"`
38 SourceID string `json:"source_id"`
39}
40
41// ShowOutput is the output schema for showing a person.
42type ShowOutput struct {
43 DeepLink string `json:"deep_link"`
44 RelationshipStrength *string `json:"relationship_strength,omitempty"`
45 Sources []ShowSource `json:"sources,omitempty"`
46 CreatedAt string `json:"created_at"`
47 UpdatedAt string `json:"updated_at"`
48}
49
50// HandleShow shows a person's details.
51func (h *Handler) HandleShow(
52 ctx context.Context,
53 _ *mcp.CallToolRequest,
54 input ShowInput,
55) (*mcp.CallToolResult, ShowOutput, error) {
56 _, id, err := lunatask.ParseReference(input.ID)
57 if err != nil {
58 return shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link"), ShowOutput{}, nil
59 }
60
61 person, err := h.client.GetPerson(ctx, id)
62 if err != nil {
63 return shared.ErrorResult(err.Error()), ShowOutput{}, nil
64 }
65
66 output := buildShowOutput(person)
67 text := formatPersonShowText(output)
68
69 return &mcp.CallToolResult{
70 Content: []mcp.Content{&mcp.TextContent{Text: text}},
71 }, output, nil
72}
73
74func buildShowOutput(person *lunatask.Person) ShowOutput {
75 output := ShowOutput{
76 CreatedAt: person.CreatedAt.Format(time.RFC3339),
77 UpdatedAt: person.UpdatedAt.Format(time.RFC3339),
78 }
79
80 output.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourcePerson, person.ID)
81
82 if person.RelationshipStrength != nil {
83 s := string(*person.RelationshipStrength)
84 output.RelationshipStrength = &s
85 }
86
87 if len(person.Sources) > 0 {
88 output.Sources = make([]ShowSource, 0, len(person.Sources))
89 for _, src := range person.Sources {
90 output.Sources = append(output.Sources, ShowSource{
91 Source: src.Source,
92 SourceID: src.SourceID,
93 })
94 }
95 }
96
97 return output
98}
99
100func formatPersonShowText(output ShowOutput) string {
101 var builder strings.Builder
102
103 builder.WriteString(fmt.Sprintf("Person: %s\n", output.DeepLink))
104
105 if output.RelationshipStrength != nil {
106 builder.WriteString(fmt.Sprintf("Relationship: %s\n", *output.RelationshipStrength))
107 }
108
109 if len(output.Sources) > 0 {
110 builder.WriteString("Sources:\n")
111
112 for _, src := range output.Sources {
113 builder.WriteString(fmt.Sprintf(" - %s: %s\n", src.Source, src.SourceID))
114 }
115 }
116
117 builder.WriteString(fmt.Sprintf("Created: %s\n", output.CreatedAt))
118 builder.WriteString("Updated: " + output.UpdatedAt)
119
120 return builder.String()
121}