list.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5// Package goal provides MCP tools for goal operations.
  6package goal
  7
  8import (
  9	"context"
 10	"fmt"
 11	"strings"
 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// ListToolName is the name of the list goals tool.
 19const ListToolName = "list_goals"
 20
 21// ListToolDescription describes the list goals tool for LLMs.
 22const ListToolDescription = `Lists goals for an area. Use list_areas to find area IDs first.
 23
 24Required:
 25- area_id: Area UUID, deep link, or config key
 26
 27Returns goal metadata (id, name, key) for the specified area.`
 28
 29// ListInput is the input schema for listing goals.
 30type ListInput struct {
 31	AreaID string `json:"area_id" jsonschema:"required"`
 32}
 33
 34// Summary represents a goal in the list output.
 35type Summary struct {
 36	ID   string `json:"id"`
 37	Name string `json:"name"`
 38	Key  string `json:"key"`
 39}
 40
 41// ListOutput is the output schema for listing goals.
 42type ListOutput struct {
 43	Goals  []Summary `json:"goals"`
 44	Count  int       `json:"count"`
 45	AreaID string    `json:"area_id"`
 46}
 47
 48// Handler handles goal tool requests.
 49type Handler struct {
 50	areas []shared.AreaProvider
 51}
 52
 53// NewHandler creates a new goal tool handler.
 54func NewHandler(areas []shared.AreaProvider) *Handler {
 55	return &Handler{areas: areas}
 56}
 57
 58// HandleList lists goals for an area.
 59func (h *Handler) HandleList(
 60	_ context.Context,
 61	_ *mcp.CallToolRequest,
 62	input ListInput,
 63) (*mcp.CallToolResult, ListOutput, error) {
 64	area := h.resolveAreaRef(input.AreaID)
 65	if area == nil {
 66		return shared.ErrorResult("unknown area: " + input.AreaID), ListOutput{}, nil
 67	}
 68
 69	summaries := make([]Summary, 0, len(area.Goals))
 70
 71	for _, goal := range area.Goals {
 72		summaries = append(summaries, Summary{
 73			ID:   goal.ID,
 74			Name: goal.Name,
 75			Key:  goal.Key,
 76		})
 77	}
 78
 79	output := ListOutput{
 80		Goals:  summaries,
 81		Count:  len(summaries),
 82		AreaID: area.ID,
 83	}
 84
 85	text := formatListText(summaries, area.Name)
 86
 87	return &mcp.CallToolResult{
 88		Content: []mcp.Content{&mcp.TextContent{Text: text}},
 89	}, output, nil
 90}
 91
 92// resolveAreaRef resolves an area reference to an AreaProvider.
 93// Accepts config key, UUID, or deep link.
 94func (h *Handler) resolveAreaRef(input string) *shared.AreaProvider {
 95	// Try UUID or deep link first
 96	if _, id, err := lunatask.ParseReference(input); err == nil {
 97		for i := range h.areas {
 98			if h.areas[i].ID == id {
 99				return &h.areas[i]
100			}
101		}
102	}
103
104	// Try config key lookup
105	for i := range h.areas {
106		if h.areas[i].Key == input {
107			return &h.areas[i]
108		}
109	}
110
111	return nil
112}
113
114func formatListText(goals []Summary, areaName string) string {
115	if len(goals) == 0 {
116		return fmt.Sprintf("No goals configured for area %q", areaName)
117	}
118
119	var text strings.Builder
120
121	text.WriteString(fmt.Sprintf("Found %d goal(s) in %q:\n", len(goals), areaName))
122
123	for _, g := range goals {
124		text.WriteString(fmt.Sprintf("- %s: %s (%s)\n", g.Key, g.Name, g.ID))
125	}
126
127	return text.String()
128}