handler.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5// Package task provides the MCP resource handler for individual Lunatask tasks.
  6package task
  7
  8import (
  9	"context"
 10	"encoding/json"
 11	"fmt"
 12	"time"
 13
 14	"git.secluded.site/go-lunatask"
 15	"github.com/modelcontextprotocol/go-sdk/mcp"
 16)
 17
 18// ResourceTemplate is the URI template for task resources.
 19const ResourceTemplate = "lunatask://task/{id}"
 20
 21// ResourceDescription describes the task resource for LLMs.
 22const ResourceDescription = `Reads metadata for a specific Lunatask task by ID or deep link.
 23
 24Due to end-to-end encryption, task name and note content are not available.
 25Returns metadata including status, priority, dates, area, and goal.
 26
 27Use list_tasks tool to discover task IDs, then read individual tasks here.`
 28
 29// taskInfo represents task metadata in the resource response.
 30type taskInfo struct {
 31	DeepLink    string  `json:"deep_link"`
 32	Status      *string `json:"status,omitempty"`
 33	Priority    *int    `json:"priority,omitempty"`
 34	Estimate    *int    `json:"estimate,omitempty"`
 35	ScheduledOn *string `json:"scheduled_on,omitempty"`
 36	CompletedAt *string `json:"completed_at,omitempty"`
 37	CreatedAt   string  `json:"created_at"`
 38	UpdatedAt   string  `json:"updated_at"`
 39	AreaID      *string `json:"area_id,omitempty"`
 40	GoalID      *string `json:"goal_id,omitempty"`
 41	Important   *bool   `json:"important,omitempty"`
 42	Urgent      *bool   `json:"urgent,omitempty"`
 43}
 44
 45// Handler handles task resource requests.
 46type Handler struct {
 47	client *lunatask.Client
 48}
 49
 50// NewHandler creates a new task resource handler.
 51func NewHandler(accessToken string) *Handler {
 52	return &Handler{
 53		client: lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
 54	}
 55}
 56
 57// HandleRead returns metadata for a specific task.
 58func (h *Handler) HandleRead(
 59	ctx context.Context,
 60	req *mcp.ReadResourceRequest,
 61) (*mcp.ReadResourceResult, error) {
 62	_, id, err := lunatask.ParseReference(req.Params.URI)
 63	if err != nil {
 64		return nil, fmt.Errorf("invalid URI %q: %w", req.Params.URI, mcp.ResourceNotFoundError(req.Params.URI))
 65	}
 66
 67	task, err := h.client.GetTask(ctx, id)
 68	if err != nil {
 69		return nil, fmt.Errorf("fetching task: %w", err)
 70	}
 71
 72	info := buildTaskInfo(task)
 73
 74	data, err := json.MarshalIndent(info, "", "  ")
 75	if err != nil {
 76		return nil, fmt.Errorf("marshaling task: %w", err)
 77	}
 78
 79	return &mcp.ReadResourceResult{
 80		Contents: []*mcp.ResourceContents{{
 81			URI:      req.Params.URI,
 82			MIMEType: "application/json",
 83			Text:     string(data),
 84		}},
 85	}, nil
 86}
 87
 88func buildTaskInfo(task *lunatask.Task) taskInfo {
 89	info := taskInfo{
 90		CreatedAt: task.CreatedAt.Format(time.RFC3339),
 91		UpdatedAt: task.UpdatedAt.Format(time.RFC3339),
 92		AreaID:    task.AreaID,
 93		GoalID:    task.GoalID,
 94	}
 95
 96	info.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
 97
 98	if task.Status != nil {
 99		s := string(*task.Status)
100		info.Status = &s
101	}
102
103	if task.Priority != nil {
104		p := int(*task.Priority)
105		info.Priority = &p
106	}
107
108	if task.Estimate != nil {
109		info.Estimate = task.Estimate
110	}
111
112	if task.ScheduledOn != nil {
113		s := task.ScheduledOn.Format("2006-01-02")
114		info.ScheduledOn = &s
115	}
116
117	if task.CompletedAt != nil {
118		s := task.CompletedAt.Format(time.RFC3339)
119		info.CompletedAt = &s
120	}
121
122	if task.Eisenhower != nil {
123		important := task.Eisenhower.IsImportant()
124		urgent := task.Eisenhower.IsUrgent()
125		info.Important = &important
126		info.Urgent = &urgent
127	}
128
129	return info
130}