feat(mcp): add semantic task list resources

Amolith created

Add MCP resources for filtered task lists, providing semantic views like
today, overdue, now, and high-priority without requiring the generic
list_tasks tool.

Static resources:
- lunatask://tasks/all (discouraged - expensive)
- lunatask://tasks/today
- lunatask://tasks/overdue
- lunatask://tasks/next-7-days
- lunatask://tasks/high-priority
- lunatask://tasks/now
- lunatask://tasks/recent-completions

Area-scoped templates (discovered via resources/templates/list):
- lunatask://area/{area}/tasks
- lunatask://area/{area}/today
- lunatask://area/{area}/overdue
- etc.

The {area} parameter accepts config keys or UUIDs.

Also default-disables list_tasks tool since resources are now the
primary interface for task discovery.

Assisted-by: Claude Opus 4.5 via Crush

Change summary

cmd/mcp/server.go                       | 126 +++++++++
internal/config/config.go               |   2 
internal/mcp/resources/areas/handler.go |  20 -
internal/mcp/resources/tasks/filters.go | 171 ++++++++++++
internal/mcp/resources/tasks/handler.go | 358 +++++++++++++++++++++++++++
5 files changed, 657 insertions(+), 20 deletions(-)

Detailed changes

cmd/mcp/server.go 🔗

@@ -18,6 +18,7 @@ import (
 	"git.secluded.site/lune/internal/mcp/resources/notebooks"
 	personrs "git.secluded.site/lune/internal/mcp/resources/person"
 	taskrs "git.secluded.site/lune/internal/mcp/resources/task"
+	"git.secluded.site/lune/internal/mcp/resources/tasks"
 	"git.secluded.site/lune/internal/mcp/shared"
 	"git.secluded.site/lune/internal/mcp/tools/habit"
 	"git.secluded.site/lune/internal/mcp/tools/journal"
@@ -44,8 +45,8 @@ func newMCPServer(cfg *config.Config, accessToken string) *mcp.Server {
 	habitProviders := shared.ToHabitProviders(cfg.Habits)
 	notebookProviders := shared.ToNotebookProviders(cfg.Notebooks)
 
-	registerResources(mcpServer, areaProviders, habitProviders, notebookProviders)
-	registerResourceTemplates(mcpServer, accessToken)
+	registerResources(mcpServer, accessToken, areaProviders, habitProviders, notebookProviders)
+	registerResourceTemplates(mcpServer, accessToken, areaProviders)
 	registerTools(mcpServer, cfg, accessToken, areaProviders, habitProviders, notebookProviders)
 
 	return mcpServer
@@ -69,6 +70,7 @@ func toAreaProviders(cfgAreas []config.Area) []shared.AreaProvider {
 
 func registerResources(
 	mcpServer *mcp.Server,
+	accessToken string,
 	areaProviders []shared.AreaProvider,
 	habitProviders []shared.HabitProvider,
 	notebookProviders []shared.NotebookProvider,
@@ -96,9 +98,11 @@ func registerResources(
 		Description: notebooks.ResourceDescription,
 		MIMEType:    "application/json",
 	}, notebooksHandler.HandleRead)
+
+	registerTaskListResources(mcpServer, accessToken, areaProviders)
 }
 
-func registerResourceTemplates(mcpServer *mcp.Server, accessToken string) {
+func registerResourceTemplates(mcpServer *mcp.Server, accessToken string, areaProviders []shared.AreaProvider) {
 	taskHandler := taskrs.NewHandler(accessToken)
 	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
 		Name:        "task",
@@ -122,6 +126,122 @@ func registerResourceTemplates(mcpServer *mcp.Server, accessToken string) {
 		Description: personrs.ResourceDescription,
 		MIMEType:    "application/json",
 	}, personHandler.HandleRead)
+
+	registerAreaTaskTemplates(mcpServer, accessToken, areaProviders)
+}
+
+func registerTaskListResources(
+	mcpServer *mcp.Server,
+	accessToken string,
+	areaProviders []shared.AreaProvider,
+) {
+	handler := tasks.NewHandler(accessToken, areaProviders)
+
+	mcpServer.AddResource(&mcp.Resource{
+		Name:        "tasks-all",
+		URI:         tasks.ResourceURIAll,
+		Description: tasks.AllDescription,
+		MIMEType:    "application/json",
+	}, handler.HandleReadAll)
+
+	mcpServer.AddResource(&mcp.Resource{
+		Name:        "tasks-today",
+		URI:         tasks.ResourceURIToday,
+		Description: tasks.TodayDescription,
+		MIMEType:    "application/json",
+	}, handler.HandleReadToday)
+
+	mcpServer.AddResource(&mcp.Resource{
+		Name:        "tasks-overdue",
+		URI:         tasks.ResourceURIOverdue,
+		Description: tasks.OverdueDescription,
+		MIMEType:    "application/json",
+	}, handler.HandleReadOverdue)
+
+	mcpServer.AddResource(&mcp.Resource{
+		Name:        "tasks-next-7-days",
+		URI:         tasks.ResourceURINext7Days,
+		Description: tasks.Next7DaysDescription,
+		MIMEType:    "application/json",
+	}, handler.HandleReadNext7Days)
+
+	mcpServer.AddResource(&mcp.Resource{
+		Name:        "tasks-high-priority",
+		URI:         tasks.ResourceURIHighPriority,
+		Description: tasks.HighPriorityDescription,
+		MIMEType:    "application/json",
+	}, handler.HandleReadHighPriority)
+
+	mcpServer.AddResource(&mcp.Resource{
+		Name:        "tasks-now",
+		URI:         tasks.ResourceURINow,
+		Description: tasks.NowDescription,
+		MIMEType:    "application/json",
+	}, handler.HandleReadNow)
+
+	mcpServer.AddResource(&mcp.Resource{
+		Name:        "tasks-recent-completions",
+		URI:         tasks.ResourceURIRecentCompletions,
+		Description: tasks.RecentCompletionsDescription,
+		MIMEType:    "application/json",
+	}, handler.HandleReadRecentCompletions)
+}
+
+func registerAreaTaskTemplates(
+	mcpServer *mcp.Server,
+	accessToken string,
+	areaProviders []shared.AreaProvider,
+) {
+	handler := tasks.NewHandler(accessToken, areaProviders)
+
+	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
+		Name:        "area-tasks",
+		URITemplate: tasks.AreaTasksTemplate,
+		Description: tasks.AreaTasksDescription,
+		MIMEType:    "application/json",
+	}, handler.HandleReadAreaTasks)
+
+	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
+		Name:        "area-tasks-today",
+		URITemplate: tasks.AreaTodayTemplate,
+		Description: tasks.AreaFilteredDescription,
+		MIMEType:    "application/json",
+	}, handler.HandleReadAreaTasks)
+
+	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
+		Name:        "area-tasks-overdue",
+		URITemplate: tasks.AreaOverdueTemplate,
+		Description: tasks.AreaFilteredDescription,
+		MIMEType:    "application/json",
+	}, handler.HandleReadAreaTasks)
+
+	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
+		Name:        "area-tasks-next-7-days",
+		URITemplate: tasks.AreaNext7DaysTemplate,
+		Description: tasks.AreaFilteredDescription,
+		MIMEType:    "application/json",
+	}, handler.HandleReadAreaTasks)
+
+	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
+		Name:        "area-tasks-high-priority",
+		URITemplate: tasks.AreaHighPriorityTemplate,
+		Description: tasks.AreaFilteredDescription,
+		MIMEType:    "application/json",
+	}, handler.HandleReadAreaTasks)
+
+	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
+		Name:        "area-tasks-now",
+		URITemplate: tasks.AreaNowTemplate,
+		Description: tasks.AreaFilteredDescription,
+		MIMEType:    "application/json",
+	}, handler.HandleReadAreaTasks)
+
+	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
+		Name:        "area-tasks-recent-completions",
+		URITemplate: tasks.AreaRecentCompletionsTempl,
+		Description: tasks.AreaFilteredDescription,
+		MIMEType:    "application/json",
+	}, handler.HandleReadAreaTasks)
 }
 
 func registerTools(

internal/config/config.go 🔗

@@ -99,7 +99,7 @@ func (t *ToolsConfig) ApplyDefaults() {
 		t.CreateTask = true
 		t.UpdateTask = true
 		t.DeleteTask = true
-		t.ListTasks = true
+		// ListTasks: default-disabled (fallback for lunatask://tasks/* resources)
 		// ShowTask: default-disabled (fallback for lunatask://task/{id} resource)
 		t.CreateNote = true
 		t.UpdateNote = true

internal/mcp/resources/areas/handler.go 🔗

@@ -18,22 +18,10 @@ import (
 const ResourceURI = "lunatask://areas"
 
 // ResourceDescription describes the areas resource for LLMs.
-const ResourceDescription = `Lists all configured Lunatask areas and their goals.
-
-Each area represents a life domain (e.g., Work, Personal, Health) and contains:
-- id: UUID to use when creating tasks in this area
-- name: Human-readable area name
-- key: Short alias for CLI usage
-- workflow: Task management style (determines which fields are relevant)
-- workflow_description: Human-readable explanation of the workflow
-- valid_statuses: Task statuses valid for this workflow
-- uses_motivation: Whether motivation field (must/should/want) is relevant
-- uses_eisenhower: Whether eisenhower matrix is relevant
-- uses_scheduling: Whether date scheduling is relevant
-- uses_priority: Whether priority field is relevant
-- goals: List of goals within the area
-
-Use workflow information to determine which task fields to set for each area.`
+const ResourceDescription = `Configured Lunatask areas and goals.
+
+Each area contains: id, name, key, workflow info, and goals.
+Use area key or id in resource templates like lunatask://area/{area}/today.`
 
 // Handler handles area resource requests.
 type Handler struct {

internal/mcp/resources/tasks/filters.go 🔗

@@ -0,0 +1,171 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package tasks
+
+import (
+	"time"
+
+	"git.secluded.site/go-lunatask"
+)
+
+// FilterFunc filters a slice of tasks.
+type FilterFunc func([]lunatask.Task) []lunatask.Task
+
+// FilterToday returns tasks scheduled for today.
+func FilterToday(tasks []lunatask.Task) []lunatask.Task {
+	today := lunatask.Today()
+	result := make([]lunatask.Task, 0)
+
+	for _, task := range tasks {
+		if task.ScheduledOn != nil && sameDay(task.ScheduledOn.Time, today.Time) {
+			result = append(result, task)
+		}
+	}
+
+	return result
+}
+
+// FilterOverdue returns tasks scheduled before today that are not completed.
+func FilterOverdue(tasks []lunatask.Task) []lunatask.Task {
+	today := lunatask.Today()
+	result := make([]lunatask.Task, 0)
+
+	for _, task := range tasks {
+		if task.ScheduledOn == nil {
+			continue
+		}
+
+		if task.ScheduledOn.Before(today.Time) && !isCompleted(&task) {
+			result = append(result, task)
+		}
+	}
+
+	return result
+}
+
+// Next7DaysWindow is the number of days to look ahead for the next 7 days filter.
+const Next7DaysWindow = 7
+
+// FilterNext7Days returns tasks scheduled within the next 7 days.
+func FilterNext7Days(tasks []lunatask.Task) []lunatask.Task {
+	today := lunatask.Today()
+	weekFromNow := lunatask.NewDate(time.Now().AddDate(0, 0, Next7DaysWindow))
+	result := make([]lunatask.Task, 0)
+
+	for _, task := range tasks {
+		if task.ScheduledOn == nil {
+			continue
+		}
+
+		// Include today through 7 days from now
+		if !task.ScheduledOn.Before(today.Time) && !task.ScheduledOn.After(weekFromNow.Time) {
+			result = append(result, task)
+		}
+	}
+
+	return result
+}
+
+// FilterHighPriority returns tasks with highest priority.
+func FilterHighPriority(tasks []lunatask.Task) []lunatask.Task {
+	result := make([]lunatask.Task, 0)
+
+	for _, task := range tasks {
+		if task.Priority != nil && *task.Priority == lunatask.PriorityHighest {
+			result = append(result, task)
+		}
+	}
+
+	return result
+}
+
+// FilterNow returns tasks that need attention now.
+// Matches tasks where ANY of:
+// - Status = "started"
+// - Priority = highest
+// - Motivation = "must"
+// - Eisenhower = urgent AND important.
+func FilterNow(tasks []lunatask.Task) []lunatask.Task {
+	result := make([]lunatask.Task, 0)
+
+	for _, task := range tasks {
+		if isNow(&task) {
+			result = append(result, task)
+		}
+	}
+
+	return result
+}
+
+//nolint:cyclop // Multiple independent conditions are inherently complex.
+func isNow(task *lunatask.Task) bool {
+	// Exclude completed tasks
+	if isCompleted(task) {
+		return false
+	}
+
+	// Status = started
+	if task.Status != nil && *task.Status == lunatask.StatusInProgress {
+		return true
+	}
+
+	// Priority = highest
+	if task.Priority != nil && *task.Priority == lunatask.PriorityHighest {
+		return true
+	}
+
+	// Motivation = must
+	if task.Motivation != nil && *task.Motivation == lunatask.MotivationMust {
+		return true
+	}
+
+	// Eisenhower = urgent AND important
+	if task.Eisenhower != nil && task.Eisenhower.IsUrgent() && task.Eisenhower.IsImportant() {
+		return true
+	}
+
+	return false
+}
+
+// RecentCompletionsWindow is the time window for recent completions.
+const RecentCompletionsWindow = 72 * time.Hour
+
+// FilterRecentCompletions returns tasks completed in the last 72 hours.
+func FilterRecentCompletions(tasks []lunatask.Task) []lunatask.Task {
+	cutoff := time.Now().Add(-RecentCompletionsWindow)
+	result := make([]lunatask.Task, 0)
+
+	for _, task := range tasks {
+		if task.CompletedAt != nil && task.CompletedAt.After(cutoff) {
+			result = append(result, task)
+		}
+	}
+
+	return result
+}
+
+// FilterByArea returns tasks in a specific area.
+func FilterByArea(tasks []lunatask.Task, areaID string) []lunatask.Task {
+	result := make([]lunatask.Task, 0)
+
+	for _, task := range tasks {
+		if task.AreaID != nil && *task.AreaID == areaID {
+			result = append(result, task)
+		}
+	}
+
+	return result
+}
+
+func isCompleted(task *lunatask.Task) bool {
+	return task.Status != nil && *task.Status == lunatask.StatusCompleted
+}
+
+func sameDay(a, b time.Time) bool {
+	y1, m1, d1 := a.Date()
+	y2, m2, d2 := b.Date()
+
+	return y1 == y2 && m1 == m2 && d1 == d2
+}

internal/mcp/resources/tasks/handler.go 🔗

@@ -0,0 +1,358 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+// Package tasks provides MCP resources for filtered task lists.
+package tasks
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strings"
+	"time"
+
+	"git.secluded.site/go-lunatask"
+	"git.secluded.site/lune/internal/mcp/shared"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
+)
+
+// ErrUnknownArea indicates the area reference could not be resolved.
+var ErrUnknownArea = errors.New("unknown area")
+
+// Resource URIs for task list filters.
+const (
+	ResourceURIAll               = "lunatask://tasks/all"
+	ResourceURIToday             = "lunatask://tasks/today"
+	ResourceURIOverdue           = "lunatask://tasks/overdue"
+	ResourceURINext7Days         = "lunatask://tasks/next-7-days"
+	ResourceURIHighPriority      = "lunatask://tasks/high-priority"
+	ResourceURINow               = "lunatask://tasks/now"
+	ResourceURIRecentCompletions = "lunatask://tasks/recent-completions"
+)
+
+// Area-scoped resource templates.
+const (
+	AreaTasksTemplate          = "lunatask://area/{area}/tasks"
+	AreaTodayTemplate          = "lunatask://area/{area}/today"
+	AreaOverdueTemplate        = "lunatask://area/{area}/overdue"
+	AreaNext7DaysTemplate      = "lunatask://area/{area}/next-7-days"
+	AreaHighPriorityTemplate   = "lunatask://area/{area}/high-priority"
+	AreaNowTemplate            = "lunatask://area/{area}/now"
+	AreaRecentCompletionsTempl = "lunatask://area/{area}/recent-completions"
+)
+
+// Resource descriptions.
+const (
+	AllDescription = `All incomplete tasks. EXPENSIVE - prefer filtered resources.`
+
+	TodayDescription = `Tasks scheduled for today.`
+
+	OverdueDescription = `Overdue tasks (scheduled before today, incomplete).`
+
+	Next7DaysDescription = `Tasks scheduled within the next 7 days.`
+
+	HighPriorityDescription = `Tasks with highest priority.`
+
+	NowDescription = `Tasks needing immediate attention: started, highest priority, must-do, or urgent+important.`
+
+	RecentCompletionsDescription = `Tasks completed in the last 72 hours.`
+
+	AreaTasksDescription = `Incomplete tasks in area. {area} accepts config key or UUID.`
+
+	AreaFilteredDescription = `Filtered tasks in area. {area} accepts config key or UUID.`
+)
+
+// Handler handles task list resource requests.
+type Handler struct {
+	client *lunatask.Client
+	areas  []shared.AreaProvider
+}
+
+// NewHandler creates a new tasks resource handler.
+func NewHandler(accessToken string, areas []shared.AreaProvider) *Handler {
+	return &Handler{
+		client: lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
+		areas:  areas,
+	}
+}
+
+// taskSummary represents a task in list output.
+type taskSummary struct {
+	DeepLink    string  `json:"deep_link"`
+	Status      *string `json:"status,omitempty"`
+	Priority    *int    `json:"priority,omitempty"`
+	ScheduledOn *string `json:"scheduled_on,omitempty"`
+	CompletedAt *string `json:"completed_at,omitempty"`
+	CreatedAt   string  `json:"created_at"`
+	AreaID      *string `json:"area_id,omitempty"`
+	GoalID      *string `json:"goal_id,omitempty"`
+	Important   *bool   `json:"important,omitempty"`
+	Urgent      *bool   `json:"urgent,omitempty"`
+}
+
+// FilterType identifies which filter to apply.
+type FilterType int
+
+// Filter type constants.
+const (
+	TypeAll FilterType = iota
+	TypeToday
+	TypeOverdue
+	TypeNext7Days
+	TypeHighPriority
+	TypeNow
+	TypeRecentCompletions
+)
+
+// HandleReadAll handles the all tasks resource.
+func (h *Handler) HandleReadAll(
+	ctx context.Context,
+	req *mcp.ReadResourceRequest,
+) (*mcp.ReadResourceResult, error) {
+	return h.handleFiltered(ctx, req, TypeAll, "")
+}
+
+// HandleReadToday handles the today tasks resource.
+func (h *Handler) HandleReadToday(
+	ctx context.Context,
+	req *mcp.ReadResourceRequest,
+) (*mcp.ReadResourceResult, error) {
+	return h.handleFiltered(ctx, req, TypeToday, "")
+}
+
+// HandleReadOverdue handles the overdue tasks resource.
+func (h *Handler) HandleReadOverdue(
+	ctx context.Context,
+	req *mcp.ReadResourceRequest,
+) (*mcp.ReadResourceResult, error) {
+	return h.handleFiltered(ctx, req, TypeOverdue, "")
+}
+
+// HandleReadNext7Days handles the next 7 days tasks resource.
+func (h *Handler) HandleReadNext7Days(
+	ctx context.Context,
+	req *mcp.ReadResourceRequest,
+) (*mcp.ReadResourceResult, error) {
+	return h.handleFiltered(ctx, req, TypeNext7Days, "")
+}
+
+// HandleReadHighPriority handles the high priority tasks resource.
+func (h *Handler) HandleReadHighPriority(
+	ctx context.Context,
+	req *mcp.ReadResourceRequest,
+) (*mcp.ReadResourceResult, error) {
+	return h.handleFiltered(ctx, req, TypeHighPriority, "")
+}
+
+// HandleReadNow handles the now tasks resource.
+func (h *Handler) HandleReadNow(
+	ctx context.Context,
+	req *mcp.ReadResourceRequest,
+) (*mcp.ReadResourceResult, error) {
+	return h.handleFiltered(ctx, req, TypeNow, "")
+}
+
+// HandleReadRecentCompletions handles the recent completions resource.
+func (h *Handler) HandleReadRecentCompletions(
+	ctx context.Context,
+	req *mcp.ReadResourceRequest,
+) (*mcp.ReadResourceResult, error) {
+	return h.handleFiltered(ctx, req, TypeRecentCompletions, "")
+}
+
+// HandleReadAreaTasks handles area-scoped task resources.
+func (h *Handler) HandleReadAreaTasks(
+	ctx context.Context,
+	req *mcp.ReadResourceRequest,
+) (*mcp.ReadResourceResult, error) {
+	areaRef, filterType := parseAreaURI(req.Params.URI)
+	if areaRef == "" {
+		return nil, fmt.Errorf("invalid URI %q: %w", req.Params.URI, mcp.ResourceNotFoundError(req.Params.URI))
+	}
+
+	areaID, err := h.resolveAreaRef(areaRef)
+	if err != nil {
+		return nil, fmt.Errorf("invalid area %q: %w", areaRef, mcp.ResourceNotFoundError(req.Params.URI))
+	}
+
+	return h.handleFiltered(ctx, req, filterType, areaID)
+}
+
+// resolveAreaRef resolves an area reference to a UUID.
+// Accepts config key or UUID.
+func (h *Handler) resolveAreaRef(input string) (string, error) {
+	// Try UUID or deep link first
+	if _, id, err := lunatask.ParseReference(input); err == nil {
+		return id, nil
+	}
+
+	// Try config key lookup
+	for _, area := range h.areas {
+		if area.Key == input {
+			return area.ID, nil
+		}
+	}
+
+	return "", fmt.Errorf("%w: %s", ErrUnknownArea, input)
+}
+
+func (h *Handler) handleFiltered(
+	ctx context.Context,
+	req *mcp.ReadResourceRequest,
+	filterType FilterType,
+	areaID string,
+) (*mcp.ReadResourceResult, error) {
+	tasks, err := h.client.ListTasks(ctx, nil)
+	if err != nil {
+		return nil, fmt.Errorf("fetching tasks: %w", err)
+	}
+
+	// Apply area filter first if specified
+	if areaID != "" {
+		tasks = FilterByArea(tasks, areaID)
+	}
+
+	// Apply semantic filter
+	tasks = applyFilter(tasks, filterType)
+
+	summaries := buildSummaries(tasks)
+
+	data, err := json.MarshalIndent(summaries, "", "  ")
+	if err != nil {
+		return nil, fmt.Errorf("marshaling tasks: %w", err)
+	}
+
+	return &mcp.ReadResourceResult{
+		Contents: []*mcp.ResourceContents{{
+			URI:      req.Params.URI,
+			MIMEType: "application/json",
+			Text:     string(data),
+		}},
+	}, nil
+}
+
+func applyFilter(tasks []lunatask.Task, filterType FilterType) []lunatask.Task {
+	switch filterType {
+	case TypeAll:
+		return filterIncomplete(tasks)
+	case TypeToday:
+		return FilterToday(tasks)
+	case TypeOverdue:
+		return FilterOverdue(tasks)
+	case TypeNext7Days:
+		return FilterNext7Days(tasks)
+	case TypeHighPriority:
+		return FilterHighPriority(tasks)
+	case TypeNow:
+		return FilterNow(tasks)
+	case TypeRecentCompletions:
+		return FilterRecentCompletions(tasks)
+	default:
+		return filterIncomplete(tasks)
+	}
+}
+
+func filterIncomplete(tasks []lunatask.Task) []lunatask.Task {
+	result := make([]lunatask.Task, 0)
+
+	for _, task := range tasks {
+		if !isCompleted(&task) {
+			result = append(result, task)
+		}
+	}
+
+	return result
+}
+
+func buildSummaries(tasks []lunatask.Task) []taskSummary {
+	summaries := make([]taskSummary, 0, len(tasks))
+
+	for _, task := range tasks {
+		summary := taskSummary{
+			CreatedAt: task.CreatedAt.Format(time.RFC3339),
+			AreaID:    task.AreaID,
+			GoalID:    task.GoalID,
+		}
+
+		summary.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
+
+		if task.Status != nil {
+			s := string(*task.Status)
+			summary.Status = &s
+		}
+
+		if task.Priority != nil {
+			p := int(*task.Priority)
+			summary.Priority = &p
+		}
+
+		if task.ScheduledOn != nil {
+			s := task.ScheduledOn.Format("2006-01-02")
+			summary.ScheduledOn = &s
+		}
+
+		if task.CompletedAt != nil {
+			s := task.CompletedAt.Format(time.RFC3339)
+			summary.CompletedAt = &s
+		}
+
+		if task.Eisenhower != nil {
+			important := task.Eisenhower.IsImportant()
+			urgent := task.Eisenhower.IsUrgent()
+			summary.Important = &important
+			summary.Urgent = &urgent
+		}
+
+		summaries = append(summaries, summary)
+	}
+
+	return summaries
+}
+
+// parseAreaURI extracts area_id and filter type from area-scoped URIs.
+// Examples:
+//   - lunatask://area/uuid/tasks -> uuid, FilterAll
+//   - lunatask://area/uuid/today -> uuid, FilterToday
+func parseAreaURI(uri string) (string, FilterType) {
+	const (
+		prefix    = "lunatask://area/"
+		maxParts  = 2
+		minLength = 1
+	)
+
+	filterNameToType := map[string]FilterType{
+		"tasks":              TypeAll,
+		"today":              TypeToday,
+		"overdue":            TypeOverdue,
+		"next-7-days":        TypeNext7Days,
+		"high-priority":      TypeHighPriority,
+		"now":                TypeNow,
+		"recent-completions": TypeRecentCompletions,
+	}
+
+	if !strings.HasPrefix(uri, prefix) {
+		return "", TypeAll
+	}
+
+	rest := strings.TrimPrefix(uri, prefix)
+	parts := strings.SplitN(rest, "/", maxParts)
+
+	if len(parts) == 0 || parts[0] == "" {
+		return "", TypeAll
+	}
+
+	areaID := parts[0]
+
+	if len(parts) == minLength {
+		return areaID, TypeAll
+	}
+
+	filterType, ok := filterNameToType[parts[1]]
+	if !ok {
+		return "", TypeAll
+	}
+
+	return areaID, filterType
+}