handler.go

 1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
 2//
 3// SPDX-License-Identifier: AGPL-3.0-or-later
 4
 5// Package timestamp provides an MCP tool for parsing natural language dates.
 6package timestamp
 7
 8import (
 9	"context"
10	"fmt"
11	"time"
12
13	"git.secluded.site/lune/internal/dateutil"
14	"github.com/modelcontextprotocol/go-sdk/mcp"
15)
16
17// ToolName is the name of this tool.
18const ToolName = "get_timestamp"
19
20// ToolDescription describes the tool for LLMs.
21const ToolDescription = `Parses natural language date/time expressions into RFC3339 timestamps.
22
23Accepts expressions like:
24- "today", "tomorrow", "yesterday"
25- "next Monday", "last Friday"
26- "2 days ago", "in 3 weeks"
27- "March 5", "2024-01-15"
28- "" (empty string returns today)
29
30Returns the timestamp in RFC3339 format (e.g., "2024-01-15T00:00:00Z").
31Use this tool to convert human-readable dates before passing them to task/habit tools.`
32
33// Input is the input schema for the timestamp tool.
34type Input struct {
35	Date string `json:"date"`
36}
37
38// Output is the output schema for the timestamp tool.
39type Output struct {
40	Timestamp string `json:"timestamp"`
41	Date      string `json:"date"`
42}
43
44// Handler handles timestamp tool requests.
45type Handler struct {
46	timezone *time.Location
47}
48
49// NewHandler creates a new timestamp handler with the given timezone.
50func NewHandler(tz string) *Handler {
51	loc, err := time.LoadLocation(tz)
52	if err != nil {
53		loc = time.UTC
54	}
55
56	return &Handler{timezone: loc}
57}
58
59// Handle parses a natural language date and returns an RFC3339 timestamp.
60func (h *Handler) Handle(
61	_ context.Context,
62	_ *mcp.CallToolRequest,
63	input Input,
64) (*mcp.CallToolResult, Output, error) {
65	parsed, err := dateutil.Parse(input.Date)
66	if err != nil {
67		return &mcp.CallToolResult{
68			IsError: true,
69			Content: []mcp.Content{
70				&mcp.TextContent{Text: err.Error()},
71			},
72		}, Output{}, nil
73	}
74
75	t := parsed.In(h.timezone)
76	output := Output{
77		Timestamp: t.Format(time.RFC3339),
78		Date:      t.Format("2006-01-02"),
79	}
80
81	inputDisplay := input.Date
82	if inputDisplay == "" {
83		inputDisplay = "(empty)"
84	}
85
86	return &mcp.CallToolResult{
87		Content: []mcp.Content{&mcp.TextContent{
88			Text: fmt.Sprintf("Parsed %q → %s", inputDisplay, output.Timestamp),
89		}},
90	}, output, nil
91}