@@ -48,6 +48,7 @@ type Config struct {
AccessToken string `toml:"access_token"`
Areas []Area `toml:"areas"`
Server ServerConfig `toml:"server"`
+ Timezone string `toml:"timezone"`
}
func main() {
@@ -84,6 +85,11 @@ func main() {
}
}
+ // Validate timezone config on startup
+ if _, err := loadLocation(config.Timezone); err != nil {
+ log.Fatalf("Timezone validation failed: %v", err)
+ }
+
mcpServer := NewMCPServer(&config)
baseURL := fmt.Sprintf("http://%s:%d", config.Server.Host, config.Server.Port)
@@ -95,6 +101,18 @@ func main() {
}
}
+// loadLocation loads a timezone location string, returning a *time.Location or error
+func loadLocation(timezone string) (*time.Location, error) {
+ if timezone == "" {
+ return nil, fmt.Errorf("Timezone is not configured; please set the 'timezone' value in your config file (e.g. 'UTC' or 'America/New_York')")
+ }
+ loc, err := time.LoadLocation(timezone)
+ if err != nil {
+ return nil, fmt.Errorf("Could not load timezone '%s': %v", timezone, err)
+ }
+ return loc, nil
+}
+
func NewMCPServer(config *Config) *server.MCPServer {
hooks := &server.Hooks{}
@@ -137,7 +155,11 @@ func NewMCPServer(config *Config) *server.MCPServer {
if !ok || natLangDate == "" {
return reportMCPError("Missing or invalid required argument: natural_language_date")
}
- parsedTime, err := anytime.Parse(natLangDate, time.Now())
+ loc, err := loadLocation(config.Timezone)
+ if err != nil {
+ return reportMCPError(err.Error())
+ }
+ parsedTime, err := anytime.Parse(natLangDate, time.Now().In(loc))
if err != nil {
return reportMCPError(fmt.Sprintf("Could not parse natural language date: %v", err))
}
@@ -241,6 +263,11 @@ func handleCreateTask(
) (*mcp.CallToolResult, error) {
arguments := request.Params.Arguments
+ // Validate timezone before proceeding any further
+ if _, err := loadLocation(config.Timezone); err != nil {
+ return reportMCPError(err.Error())
+ }
+
areaID, ok := arguments["area_id"].(string)
if !ok || areaID == "" {
return reportMCPError("Missing or invalid required argument: area_id")
@@ -270,6 +297,19 @@ func handleCreateTask(
}
}
+ // Validate scheduled_on format if provided
+ if scheduledOnArg, exists := arguments["scheduled_on"]; exists {
+ if scheduledOnStr, ok := scheduledOnArg.(string); ok && scheduledOnStr != "" {
+ if _, err := time.Parse(time.RFC3339, scheduledOnStr); err != nil {
+ return reportMCPError(fmt.Sprintf("Invalid format for scheduled_on: '%s'. Must be RFC3339 timestamp (e.g., YYYY-MM-DDTHH:MM:SSZ). Use get_task_timestamp tool first.", scheduledOnStr))
+ }
+ } else if !ok {
+ // It exists but isn't a string, which shouldn't happen based on MCP schema but check anyway
+ return reportMCPError("Invalid type for scheduled_on argument: expected string.")
+ }
+ // If it's an empty string, it's handled by the API or omitempty later, no need to validate format.
+ }
+
var payload LunataskCreateTaskRequest
argBytes, err := json.Marshal(arguments)
if err != nil {
@@ -371,6 +411,7 @@ func createDefaultConfigFile(configPath string) {
Port: 8080,
},
AccessToken: "",
+ Timezone: "UTC",
Areas: []Area{{
Name: "Example Area",
ID: "area-id-placeholder",
@@ -388,6 +429,6 @@ func createDefaultConfigFile(configPath string) {
if err := toml.NewEncoder(file).Encode(defaultConfig); err != nil {
log.Fatalf("Failed to encode default config to %s: %v", configPath, err)
}
- fmt.Printf("A default config has been created at %s.\nPlease edit it to provide your Lunatask access token and correct area/goal IDs, then restart the server.\n", configPath)
+ fmt.Printf("A default config has been created at %s.\nPlease edit it to provide your Lunatask access token, correct area/goal IDs, and your timezone (IANA/Olson format, e.g. 'America/New_York'), then restart the server.\n", configPath)
os.Exit(1)
}