1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5// Package areas provides the MCP resource handler for Lunatask areas and goals.
6package areas
7
8import (
9 "context"
10 "encoding/json"
11 "fmt"
12
13 "git.secluded.site/lune/internal/mcp/shared"
14 "github.com/modelcontextprotocol/go-sdk/mcp"
15)
16
17// ResourceURI is the URI for the areas resource.
18const ResourceURI = "lunatask://areas"
19
20// ResourceDescription describes the areas resource for LLMs.
21const ResourceDescription = `Configured Lunatask areas and goals.
22
23Each area contains: id, name, key, workflow info, and goals.
24Use area key or id in resource templates like lunatask://area/{area}/today.`
25
26// Handler handles area resource requests.
27type Handler struct {
28 areas []shared.AreaProvider
29}
30
31// NewHandler creates a new areas resource handler.
32func NewHandler(areas []shared.AreaProvider) *Handler {
33 return &Handler{areas: areas}
34}
35
36// areaInfo represents an area in the resource response.
37type areaInfo struct {
38 ID string `json:"id"`
39 Name string `json:"name"`
40 Key string `json:"key"`
41 Workflow string `json:"workflow"`
42 WorkflowDescription string `json:"workflow_description"`
43 ValidStatuses []string `json:"valid_statuses"`
44 UsesMotivation bool `json:"uses_motivation"`
45 UsesEisenhower bool `json:"uses_eisenhower"`
46 UsesScheduling bool `json:"uses_scheduling"`
47 UsesPriority bool `json:"uses_priority"`
48 Goals []goalInfo `json:"goals"`
49}
50
51// goalInfo represents a goal in the resource response.
52type goalInfo struct {
53 ID string `json:"id"`
54 Name string `json:"name"`
55 Key string `json:"key"`
56}
57
58// HandleRead returns the configured areas and goals.
59func (h *Handler) HandleRead(
60 _ context.Context,
61 _ *mcp.ReadResourceRequest,
62) (*mcp.ReadResourceResult, error) {
63 areasInfo := make([]areaInfo, 0, len(h.areas))
64
65 for _, area := range h.areas {
66 goals := make([]goalInfo, 0, len(area.Goals))
67 for _, g := range area.Goals {
68 goals = append(goals, goalInfo{
69 ID: g.ID,
70 Name: g.Name,
71 Key: g.Key,
72 })
73 }
74
75 validStatuses := make([]string, 0, len(area.Workflow.ValidStatuses()))
76 for _, s := range area.Workflow.ValidStatuses() {
77 validStatuses = append(validStatuses, string(s))
78 }
79
80 areasInfo = append(areasInfo, areaInfo{
81 ID: area.ID,
82 Name: area.Name,
83 Key: area.Key,
84 Workflow: string(area.Workflow),
85 WorkflowDescription: area.Workflow.Description(),
86 ValidStatuses: validStatuses,
87 UsesMotivation: area.Workflow.UsesMotivation(),
88 UsesEisenhower: area.Workflow.UsesEisenhower(),
89 UsesScheduling: area.Workflow.UsesScheduling(),
90 UsesPriority: area.Workflow.UsesPriority(),
91 Goals: goals,
92 })
93 }
94
95 data, err := json.MarshalIndent(areasInfo, "", " ")
96 if err != nil {
97 return nil, fmt.Errorf("marshaling areas: %w", err)
98 }
99
100 return &mcp.ReadResourceResult{
101 Contents: []*mcp.ResourceContents{{
102 URI: ResourceURI,
103 MIMEType: "application/json",
104 Text: string(data),
105 }},
106 }, nil
107}