handler.go

 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 = `Lists all configured Lunatask areas and their goals.
22
23Each area represents a life domain (e.g., Work, Personal, Health) and contains:
24- id: UUID to use when creating tasks in this area
25- name: Human-readable area name
26- key: Short alias for CLI usage
27- goals: List of goals within the area, each with id, name, and key
28
29Use this resource to discover valid area and goal IDs before creating or updating tasks.`
30
31// Handler handles area resource requests.
32type Handler struct {
33	areas []shared.AreaProvider
34}
35
36// NewHandler creates a new areas resource handler.
37func NewHandler(areas []shared.AreaProvider) *Handler {
38	return &Handler{areas: areas}
39}
40
41// areaInfo represents an area in the resource response.
42type areaInfo struct {
43	ID    string     `json:"id"`
44	Name  string     `json:"name"`
45	Key   string     `json:"key"`
46	Goals []goalInfo `json:"goals"`
47}
48
49// goalInfo represents a goal in the resource response.
50type goalInfo struct {
51	ID   string `json:"id"`
52	Name string `json:"name"`
53	Key  string `json:"key"`
54}
55
56// HandleRead returns the configured areas and goals.
57func (h *Handler) HandleRead(
58	_ context.Context,
59	_ *mcp.ReadResourceRequest,
60) (*mcp.ReadResourceResult, error) {
61	areasInfo := make([]areaInfo, 0, len(h.areas))
62
63	for _, area := range h.areas {
64		goals := make([]goalInfo, 0, len(area.Goals))
65		for _, g := range area.Goals {
66			goals = append(goals, goalInfo{
67				ID:   g.ID,
68				Name: g.Name,
69				Key:  g.Key,
70			})
71		}
72
73		areasInfo = append(areasInfo, areaInfo{
74			ID:    area.ID,
75			Name:  area.Name,
76			Key:   area.Key,
77			Goals: goals,
78		})
79	}
80
81	data, err := json.MarshalIndent(areasInfo, "", "  ")
82	if err != nil {
83		return nil, fmt.Errorf("marshaling areas: %w", err)
84	}
85
86	return &mcp.ReadResourceResult{
87		Contents: []*mcp.ResourceContents{{
88			URI:      ResourceURI,
89			MIMEType: "application/json",
90			Text:     string(data),
91		}},
92	}, nil
93}