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 = `Parse natural language dates into RFC3339 timestamps.
22
23Use before passing dates to other tools. Supports PHP strtotime syntax:
24relative (+3 days, next Monday), named (March 5), ISO (2024-01-15).
25Empty input returns today.`
26
27// ToolAnnotations returns hints about tool behavior.
28func ToolAnnotations() *mcp.ToolAnnotations {
29 return &mcp.ToolAnnotations{
30 ReadOnlyHint: true,
31 }
32}
33
34// Input is the input schema for the timestamp tool.
35type Input struct {
36 Date string `json:"date" jsonschema:"Date/time expression to parse (empty = today)"`
37}
38
39// Output is the output schema for the timestamp tool.
40type Output struct {
41 Timestamp string `json:"timestamp"`
42 Date string `json:"date"`
43}
44
45// Handler handles timestamp tool requests.
46type Handler struct {
47 timezone *time.Location
48}
49
50// NewHandler creates a new timestamp handler with the given timezone.
51func NewHandler(tz string) *Handler {
52 loc, err := time.LoadLocation(tz)
53 if err != nil {
54 loc = time.UTC
55 }
56
57 return &Handler{timezone: loc}
58}
59
60// Handle parses a natural language date and returns an RFC3339 timestamp.
61func (h *Handler) Handle(
62 _ context.Context,
63 _ *mcp.CallToolRequest,
64 input Input,
65) (*mcp.CallToolResult, Output, error) {
66 parsed, err := dateutil.ParseInTZ(input.Date, h.timezone)
67 if err != nil {
68 return &mcp.CallToolResult{
69 IsError: true,
70 Content: []mcp.Content{
71 &mcp.TextContent{Text: err.Error()},
72 },
73 }, Output{}, nil
74 }
75
76 t := parsed.In(h.timezone)
77 output := Output{
78 Timestamp: t.Format(time.RFC3339),
79 Date: t.Format("2006-01-02"),
80 }
81
82 inputDisplay := input.Date
83 if inputDisplay == "" {
84 inputDisplay = "(empty)"
85 }
86
87 return &mcp.CallToolResult{
88 Content: []mcp.Content{&mcp.TextContent{
89 Text: fmt.Sprintf("Parsed %q → %s", inputDisplay, output.Timestamp),
90 }},
91 }, output, nil
92}