handler.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5// Package note provides the MCP resource handler for individual Lunatask notes.
  6package note
  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 note resources.
 19const ResourceTemplate = "lunatask://note/{id}"
 20
 21// ResourceDescription describes the note resource for LLMs.
 22const ResourceDescription = `Reads metadata for a specific Lunatask note by ID or deep link.
 23
 24Due to end-to-end encryption, note title and content are not available.
 25Returns metadata including notebook, date, pinned status, and sources.`
 26
 27// sourceInfo represents a source reference in the response.
 28type sourceInfo struct {
 29	Source   string `json:"source"`
 30	SourceID string `json:"source_id"`
 31}
 32
 33// noteInfo represents note metadata in the resource response.
 34type noteInfo struct {
 35	DeepLink   string       `json:"deep_link"`
 36	NotebookID *string      `json:"notebook_id,omitempty"`
 37	DateOn     *string      `json:"date_on,omitempty"`
 38	Pinned     bool         `json:"pinned"`
 39	Sources    []sourceInfo `json:"sources,omitempty"`
 40	CreatedAt  string       `json:"created_at"`
 41	UpdatedAt  string       `json:"updated_at"`
 42}
 43
 44// Handler handles note resource requests.
 45type Handler struct {
 46	client *lunatask.Client
 47}
 48
 49// NewHandler creates a new note resource handler.
 50func NewHandler(accessToken string) *Handler {
 51	return &Handler{
 52		client: lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
 53	}
 54}
 55
 56// HandleRead returns metadata for a specific note.
 57func (h *Handler) HandleRead(
 58	ctx context.Context,
 59	req *mcp.ReadResourceRequest,
 60) (*mcp.ReadResourceResult, error) {
 61	_, id, err := lunatask.ParseReference(req.Params.URI)
 62	if err != nil {
 63		return nil, fmt.Errorf("invalid URI %q: %w", req.Params.URI, mcp.ResourceNotFoundError(req.Params.URI))
 64	}
 65
 66	note, err := h.client.GetNote(ctx, id)
 67	if err != nil {
 68		return nil, fmt.Errorf("fetching note: %w", err)
 69	}
 70
 71	info := buildNoteInfo(note)
 72
 73	data, err := json.MarshalIndent(info, "", "  ")
 74	if err != nil {
 75		return nil, fmt.Errorf("marshaling note: %w", err)
 76	}
 77
 78	return &mcp.ReadResourceResult{
 79		Contents: []*mcp.ResourceContents{{
 80			URI:      req.Params.URI,
 81			MIMEType: "application/json",
 82			Text:     string(data),
 83		}},
 84	}, nil
 85}
 86
 87func buildNoteInfo(note *lunatask.Note) noteInfo {
 88	info := noteInfo{
 89		NotebookID: note.NotebookID,
 90		Pinned:     note.Pinned,
 91		CreatedAt:  note.CreatedAt.Format(time.RFC3339),
 92		UpdatedAt:  note.UpdatedAt.Format(time.RFC3339),
 93	}
 94
 95	info.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourceNote, note.ID)
 96
 97	if note.DateOn != nil {
 98		s := note.DateOn.Format("2006-01-02")
 99		info.DateOn = &s
100	}
101
102	if len(note.Sources) > 0 {
103		info.Sources = make([]sourceInfo, 0, len(note.Sources))
104		for _, src := range note.Sources {
105			info.Sources = append(info.Sources, sourceInfo{
106				Source:   src.Source,
107				SourceID: src.SourceID,
108			})
109		}
110	}
111
112	return info
113}