tdl_mcp_server.py•35.1 kB
#!/usr/bin/env python3
"""
MCP Server for ToDoList (.tdl) XML files
Provides tools to read, create, update, search, and manage tasks in ToDoList application format.
Monitors D:\Projects\mylist.tdl by default.
Features:
- Read all tasks or filter by date
- Create new tasks with full metadata
- Update existing tasks (title, description, priority, due date, category, assignment, progress)
- Search and filter tasks by various criteria
- Full ToDoList XML format compatibility
"""
import asyncio
import json
import xml.etree.ElementTree as ET
from datetime import datetime, date, timedelta
from pathlib import Path
from typing import Any, Dict, List, Optional, Literal, Tuple
import re
from pydantic import BaseModel, Field
from mcp.server.fastmcp import FastMCP
from mcp.server.models import InitializationOptions
from mcp.server import NotificationOptions
# Initialize MCP server with FastMCP
mcp = FastMCP("todolist-mcp-server", "0.4.0")
# Default file path
DEFAULT_TDL_FILE = r"D:\Projects\mylist.tdl"
class ToDoListManager:
"""Manages ToDoList application XML-based .tdl files"""
def __init__(self, base_path: str = None, default_file: str = DEFAULT_TDL_FILE):
self.base_path = Path(base_path) if base_path else Path.cwd()
self.default_file = default_file
def parse_tdl_file(self, file_path: str = None) -> ET.ElementTree:
"""Parse a .tdl XML file"""
if file_path is None:
file_path = self.default_file
try:
tree = ET.parse(file_path)
return tree
except ET.ParseError as e:
raise ValueError(f"Invalid XML format in {file_path}: {e}")
except FileNotFoundError:
raise FileNotFoundError(f"File not found: {file_path}")
def extract_tasks(self, tree: ET.ElementTree) -> List[Dict[str, Any]]:
"""Extract tasks from ToDoList XML format in a hierarchical structure"""
root = tree.getroot()
return self._extract_tasks_recursive(root)
def _extract_tasks_recursive(self, parent_element: ET.Element) -> List[Dict[str, Any]]:
"""Recursively extract tasks from a parent element."""
tasks = []
for task_elem in parent_element.findall('./TASK'):
task = {}
# Extract from attributes
task['id'] = task_elem.get('ID', '')
task['title'] = task_elem.get('TITLE', '')
task['priority'] = self._decode_priority(task_elem.get('PRIORITY', ''))
task['status'] = self._decode_status(task_elem)
task['due_date'] = self._decode_date(task_elem.get('DUEDATE', ''))
task['created_date'] = self._decode_date(task_elem.get('CREATIONDATE', ''))
task['percent_done'] = task_elem.get('PERCENTDONE', '0')
task['comments'] = task_elem.get('COMMENTS', '')
task['description'] = task['comments']
task['pos'] = task_elem.get('POS', '')
task['pos_string'] = task_elem.get('POSSTRING', '')
# Extract categories
categories = [cat.text.strip() for cat in task_elem.findall('CATEGORY') if cat.text]
task['category'] = ', '.join(categories)
# Extract allocations
allocations = [alloc.text.strip() for alloc in task_elem.findall('ALLOCATEDTO') if alloc.text]
task['allocated_to'] = ', '.join(allocations)
# Calculate completion status
percent = int(task['percent_done']) if task['percent_done'].isdigit() else 0
task['completed'] = percent >= 100
# Recursively extract children
task['children'] = self._extract_tasks_recursive(task_elem)
tasks.append(task)
return tasks
def _decode_priority(self, priority_str: str) -> str:
"""Decode ToDoList priority values"""
if not priority_str or priority_str == '0':
return 'Normal'
try:
priority_val = int(priority_str)
if priority_val <= -2:
return 'Low'
elif priority_val == -1:
return 'Below Normal'
elif priority_val == 0:
return 'Normal'
elif priority_val == 1:
return 'Above Normal'
else:
return 'High'
except ValueError:
return 'Normal'
def _encode_priority(self, priority_str: str) -> str:
"""Encode priority to ToDoList format"""
priority_map = {
'Low': '-2',
'Below Normal': '-1',
'Normal': '0',
'Above Normal': '1',
'High': '2',
'Urgent': '3'
}
return priority_map.get(priority_str, '0')
def _decode_status(self, task_elem: ET.Element) -> str:
"""Decode task status from various fields"""
percent_done = task_elem.get('PERCENTDONE', '0')
if percent_done == '100':
return 'Completed'
elif percent_done == '0':
return 'Not Started'
else:
return f'{percent_done}% Complete'
def _decode_date(self, date_str: str) -> str:
"""Decode ToDoList date format (Excel serial date)"""
if not date_str:
return ''
try:
# ToDoList uses Excel date serial format
excel_date = float(date_str)
# Excel epoch starts 1900-01-01, but has a bug counting 1900 as leap year
excel_epoch = datetime(1899, 12, 30) # Adjusted for Excel bug
python_date = excel_epoch + timedelta(days=excel_date)
return python_date.strftime('%Y-%m-%d')
except (ValueError, TypeError):
return date_str
def _encode_date(self, date_str: str) -> str:
"""Encode date to ToDoList Excel serial format"""
if not date_str:
return ''
try:
date_obj = datetime.strptime(date_str, '%Y-%m-%d')
excel_epoch = datetime(1899, 12, 30)
delta = date_obj - excel_epoch
return str(delta.days + (delta.seconds / 86400))
except ValueError:
return ''
def _find_task_by_pos_string(self, root: ET.Element, pos_string: str) -> Optional[ET.Element]:
"""Find a task element by its POSSTRING."""
if not pos_string:
return root
parts = pos_string.split('.')
current_element = root
for i, part in enumerate(parts):
pos = int(part) - 1
children = sorted(current_element.findall('./TASK'), key=lambda t: int(t.get('POS', 0)))
if pos < len(children):
current_element = children[pos]
# Final part of the path, we found our element
if i == len(parts) - 1:
return current_element
else:
return None
return None
def _update_positions(self, parent_element: ET.Element):
"""Update the POS and POSSTRING of all child tasks of a given element."""
parent_pos_string = parent_element.get('POSSTRING', '')
for i, task_elem in enumerate(parent_element.findall('./TASK')):
new_pos = str(i)
if parent_pos_string:
new_pos_string = f"{parent_pos_string}.{i + 1}"
else:
new_pos_string = str(i + 1)
task_elem.set('POS', new_pos)
task_elem.set('POSSTRING', new_pos_string)
# Recursively update positions for children
self._update_positions(task_elem)
def get_next_unique_id(self, root: ET.Element) -> str:
"""Get the next unique ID from ToDoList structure"""
next_id_str = root.get('NEXTUNIQUEID', '1')
try:
next_id = int(next_id_str)
# Update the NEXTUNIQUEID attribute
root.set('NEXTUNIQUEID', str(next_id + 1))
return str(next_id)
except ValueError:
return '1'
def _save_tdl_file(self, tree: ET.ElementTree, file_path: str):
"""Save ToDoList file preserving format"""
# Write with XML declaration
tree.write(file_path, encoding='utf-8', xml_declaration=True)
def update_task(self, task_id: str, file_path: str = None, **updates) -> Tuple[bool, str]:
"""Update an existing task in ToDoList format"""
if file_path is None:
file_path = self.default_file
try:
if not Path(file_path).exists():
return False, f"File not found: {file_path}"
tree = self.parse_tdl_file(file_path)
root = tree.getroot()
# Find the task by ID
task_elem = None
for task in root.findall('.//TASK'):
if task.get('ID') == task_id:
task_elem = task
break
if task_elem is None:
return False, f"Task with ID '{task_id}' not found"
# Track what was updated
updated_fields = []
# Update basic attributes
if 'title' in updates and updates['title'] is not None:
task_elem.set('TITLE', updates['title'])
updated_fields.append('title')
if 'description' in updates and updates['description'] is not None:
task_elem.set('COMMENTS', updates['description'])
updated_fields.append('description')
if 'priority' in updates and updates['priority'] is not None:
priority_encoded = self._encode_priority(updates['priority'])
task_elem.set('PRIORITY', priority_encoded)
task_elem.set('RISK', priority_encoded) # ToDoList uses same value
updated_fields.append('priority')
if 'percent_done' in updates and updates['percent_done'] is not None:
task_elem.set('PERCENTDONE', str(updates['percent_done']))
updated_fields.append('percent_done')
# Handle due date (support clearing with empty string)
if 'due_date' in updates:
if updates['due_date'] == '':
# Clear due date
if 'DUEDATE' in task_elem.attrib:
del task_elem.attrib['DUEDATE']
if 'DUEDATESTRING' in task_elem.attrib:
del task_elem.attrib['DUEDATESTRING']
updated_fields.append('due_date (cleared)')
elif updates['due_date'] is not None:
# Set new due date
due_date_encoded = self._encode_date(updates['due_date'])
if due_date_encoded:
task_elem.set('DUEDATE', due_date_encoded)
due_date_obj = datetime.strptime(updates['due_date'], '%Y-%m-%d')
task_elem.set('DUEDATESTRING', due_date_obj.strftime('%d/%m/%Y'))
updated_fields.append('due_date')
# Handle category (support clearing with empty string)
if 'category' in updates:
# Remove existing category elements
for cat_elem in task_elem.findall('CATEGORY'):
task_elem.remove(cat_elem)
if updates['category'] == '':
updated_fields.append('category (cleared)')
elif updates['category'] is not None:
# Add new category
category_elem = ET.SubElement(task_elem, 'CATEGORY')
category_elem.text = updates['category']
updated_fields.append('category')
# Handle allocated_to (support clearing with empty string)
if 'allocated_to' in updates:
# Remove existing allocation elements
for alloc_elem in task_elem.findall('ALLOCATEDTO'):
task_elem.remove(alloc_elem)
if updates['allocated_to'] == '':
updated_fields.append('allocated_to (cleared)')
elif updates['allocated_to'] is not None:
# Add new allocation
# Split by comma if multiple people
allocations = [a.strip() for a in updates['allocated_to'].split(',') if a.strip()]
for allocation in allocations:
alloc_elem = ET.SubElement(task_elem, 'ALLOCATEDTO')
alloc_elem.text = allocation
updated_fields.append('allocated_to')
# Update modification info if any changes were made
if updated_fields:
now = datetime.now()
mod_date = self._encode_date(now.strftime('%Y-%m-%d'))
task_elem.set('LASTMOD', mod_date)
task_elem.set('LASTMODSTRING', now.strftime('%d/%m/%Y %I:%M %p'))
task_elem.set('LASTMODBY', 'CLAUDE')
# Update root modification time too
root.set('LASTMOD', mod_date)
root.set('LASTMODSTRING', now.strftime('%d/%m/%Y %I:%M %p'))
# Save the file
self._save_tdl_file(tree, file_path)
return True, f"Successfully updated task '{task_id}': {', '.join(updated_fields)}"
else:
return False, "No updates specified or all values were None"
except Exception as e:
return False, f"Error updating task: {str(e)}"
def filter_tasks_by_date(self, tasks: List[Dict], target_date: date = None) -> List[Dict]:
"""Filter tasks by due date (defaults to today)"""
if target_date is None:
target_date = date.today()
today_tasks = []
for task in tasks:
due_date_str = task.get('due_date', '')
if not due_date_str:
continue
try:
due_date = datetime.strptime(due_date_str, '%Y-%m-%d').date()
if due_date == target_date:
today_tasks.append(task)
except ValueError:
continue
return today_tasks
def search_tasks(self, tasks: List[Dict], **filters) -> List[Dict]:
"""Search and filter tasks based on various criteria"""
filtered_tasks = tasks.copy()
# Filter by search term (title and description)
if filters.get('search_term'):
search_term = filters['search_term'].lower()
filtered_tasks = [
task for task in filtered_tasks
if search_term in task.get('title', '').lower() or
search_term in task.get('description', '').lower()
]
# Filter by category
if filters.get('category'):
category = filters['category'].lower()
filtered_tasks = [
task for task in filtered_tasks
if category in task.get('category', '').lower()
]
# Filter by priority
if filters.get('priority'):
priority = filters['priority']
filtered_tasks = [
task for task in filtered_tasks
if task.get('priority') == priority
]
# Filter by completion status
if filters.get('completed') is not None:
completed = filters['completed']
filtered_tasks = [
task for task in filtered_tasks
if task.get('completed', False) == completed
]
# Filter by assigned person
if filters.get('assigned_to'):
assigned = filters['assigned_to'].lower()
filtered_tasks = [
task for task in filtered_tasks
if assigned in task.get('allocated_to', '').lower()
]
return filtered_tasks
def format_tasks_as_markdown(self, tasks: List[Dict]) -> str:
"""Convert tasks to Markdown format, preserving hierarchy."""
if not tasks:
return "No tasks found."
md_lines = ["# Tasks\n"]
self._format_tasks_recursive(tasks, md_lines, level=0)
return "\n".join(md_lines)
def _format_tasks_recursive(self, tasks: List[Dict], md_lines: List[str], level: int):
"""Recursively format tasks with indentation."""
indent = " " * level
for task in sorted(tasks, key=lambda t: int(t.get('pos', 0))):
title = task.get('title', 'Untitled Task')
completed = task.get('completed', False)
pos_string = task.get('pos_string', '')
task_id = task.get('id', '')
checkbox = "- [x]" if completed else "- [ ]"
md_lines.append(f"{indent}{checkbox} {pos_string} (ID: {task_id}) **{title}**")
details_indent = indent + " "
if task.get('description'):
md_lines.append(f"{details_indent}- Description: {task['description']}")
if task.get('due_date'):
md_lines.append(f"{details_indent}- Due: {task['due_date']}")
if task.get('priority') and task.get('priority') != 'Normal':
md_lines.append(f"{details_indent}- Priority: {task['priority']}")
if task.get('category'):
md_lines.append(f"{details_indent}- Category: {task['category']}")
if task.get('allocated_to'):
md_lines.append(f"{details_indent}- Assigned to: {task['allocated_to']}")
if task.get('percent_done', '0') != '0':
md_lines.append(f"{details_indent}- Progress: {task['percent_done']}%")
md_lines.append("")
if task.get('children'):
self._format_tasks_recursive(task['children'], md_lines, level + 1)
todo_manager = ToDoListManager()
class GetTasksArgs(BaseModel):
format: Literal['markdown', 'json'] = Field("markdown", description="Output format")
@mcp.tool()
def get_my_tasks(args: GetTasksArgs) -> str:
"""Get all tasks from your main ToDoList file"""
if not Path(DEFAULT_TDL_FILE).exists():
return f"Your main todolist file doesn't exist: {DEFAULT_TDL_FILE}"
try:
tree = todo_manager.parse_tdl_file()
tasks = todo_manager.extract_tasks(tree)
if args.format == "json":
json_tasks = [{k: v for k, v in task.items() if k != 'xml_element'} for task in tasks]
return json.dumps(json_tasks, indent=2)
else:
return f"# Your Tasks ({DEFAULT_TDL_FILE})\n\n" + todo_manager.format_tasks_as_markdown(tasks)
except Exception as e:
return f"Error reading tasks: {str(e)}"
class GetTodayTasksArgs(BaseModel):
target_date: Optional[str] = Field(None, description="Target date in YYYY-MM-DD format (defaults to today)")
format: Literal['markdown', 'json'] = Field("markdown", description="Output format")
@mcp.tool()
def get_today_tasks(args: GetTodayTasksArgs) -> str:
"""Get today's tasks from your main ToDoList file"""
target_d = date.today()
if args.target_date:
try:
target_d = datetime.strptime(args.target_date, '%Y-%m-%d').date()
except ValueError:
return f"Invalid date format: {args.target_date}. Use YYYY-MM-DD"
if not Path(DEFAULT_TDL_FILE).exists():
return f"Your main todolist file doesn't exist: {DEFAULT_TDL_FILE}"
try:
tree = todo_manager.parse_tdl_file()
all_tasks = todo_manager.extract_tasks(tree)
today_tasks = todo_manager.filter_tasks_by_date(all_tasks, target_d)
if args.format == "json":
json_tasks = [{k: v for k, v in task.items() if k != 'xml_element'} for task in today_tasks]
return json.dumps(json_tasks, indent=2)
else:
date_str = target_d.strftime('%Y-%m-%d')
if today_tasks:
return f"# Tasks due on {date_str}\n\n" + todo_manager.format_tasks_as_markdown(today_tasks)
else:
return f"No tasks due on {date_str}"
except Exception as e:
return f"Error reading tasks: {str(e)}"
class AddTaskArgs(BaseModel):
title: str = Field(..., description="Task title")
position: Optional[str] = Field(None, description="Position to add the task (e.g., '6.3.4' or parent position '5.3.7')")
description: Optional[str] = Field(None, description="Task description")
due_date: Optional[str] = Field(None, description="Due date in YYYY-MM-DD format")
priority: Literal['Low', 'Below Normal', 'Normal', 'Above Normal', 'High', 'Urgent'] = Field("Normal", description="Task priority")
category: Optional[str] = Field(None, description="Task category or project")
class UpdateTaskArgs(BaseModel):
task_id: str = Field(..., description="ID of the task to update")
title: Optional[str] = Field(None, description="New task title")
description: Optional[str] = Field(None, description="New task description")
due_date: Optional[str] = Field(None, description="New due date in YYYY-MM-DD format (empty string to clear)")
priority: Optional[Literal['Low', 'Below Normal', 'Normal', 'Above Normal', 'High', 'Urgent']] = Field(None, description="New task priority")
category: Optional[str] = Field(None, description="New task category or project (empty string to clear)")
percent_done: Optional[int] = Field(None, description="Completion percentage (0-100)", ge=0, le=100)
allocated_to: Optional[str] = Field(None, description="Person(s) assigned to task (empty string to clear)")
class SearchTasksArgs(BaseModel):
search_term: Optional[str] = Field(None, description="Search in task titles and descriptions")
category: Optional[str] = Field(None, description="Filter by category")
priority: Optional[Literal['Low', 'Below Normal', 'Normal', 'Above Normal', 'High', 'Urgent']] = Field(None, description="Filter by priority")
completed: Optional[bool] = Field(None, description="Filter by completion status")
assigned_to: Optional[str] = Field(None, description="Filter by person assigned")
format: Literal['markdown', 'json'] = Field("markdown", description="Output format")
class MoveTaskArgs(BaseModel):
task_id: str = Field(..., description="ID of the task to move")
new_position: str = Field(..., description="New position for the task (e.g., '6.3.4' or parent position '5.3.7')")
class GetTaskArgs(BaseModel):
task_id: str = Field(..., description="ID of the task to retrieve")
format: Literal['markdown', 'json'] = Field("markdown", description="Output format")
@mcp.tool()
def add_task(args: AddTaskArgs) -> str:
"""Add a new task to your main ToDoList file"""
if not Path(DEFAULT_TDL_FILE).exists():
return f"Error: ToDoList file doesn't exist: {DEFAULT_TDL_FILE}. Please open ToDoList first to create the file."
try:
tree = todo_manager.parse_tdl_file()
root = tree.getroot()
task_data = args.dict()
# Find parent and insertion index
parent_pos_string = ""
insert_index = -1
if args.position:
parts = args.position.split('.')
# Check if the position is to insert at a specific index or to append to a parent
is_insert = len(root.findall(f".//TASK[@POSSTRING='{args.position}']")) > 0
if is_insert:
parent_pos_string = ".".join(parts[:-1])
insert_index = int(parts[-1]) - 1
else: # Append
parent_pos_string = args.position
parent_element = todo_manager._find_task_by_pos_string(root, parent_pos_string)
if parent_element is None:
if not parent_pos_string: # Top-level
parent_element = root
else:
return f"Error: Could not find parent task at position {parent_pos_string}"
# Get next unique ID
task_id = todo_manager.get_next_unique_id(root)
# Create new TASK element
new_task = ET.Element('TASK')
new_task.set('ID', task_id)
new_task.set('TITLE', task_data.get('title', ''))
# Set other attributes...
now = datetime.now()
creation_date = todo_manager._encode_date(now.strftime('%Y-%m-%d'))
new_task.set('CREATIONDATE', creation_date)
new_task.set('CREATIONDATESTRING', now.strftime('%d/%m/%Y %I:%M %p'))
new_task.set('LASTMOD', creation_date)
new_task.set('LASTMODSTRING', now.strftime('%d/%m/%Y %I:%M %p'))
new_task.set('LASTMODBY', 'CLAUDE')
if task_data.get('description'):
new_task.set('COMMENTS', task_data['description'])
priority_encoded = todo_manager._encode_priority(task_data.get('priority', 'Normal'))
new_task.set('PRIORITY', priority_encoded)
if task_data.get('due_date'):
due_date_encoded = todo_manager._encode_date(task_data['due_date'])
if due_date_encoded:
new_task.set('DUEDATE', due_date_encoded)
due_date_obj = datetime.strptime(task_data['due_date'], '%Y-%m-%d')
new_task.set('DUEDATESTRING', due_date_obj.strftime('%d/%m/%Y'))
if task_data.get('category'):
category_elem = ET.SubElement(new_task, 'CATEGORY')
category_elem.text = task_data['category']
# Insert and update positions
if insert_index != -1:
parent_element.insert(insert_index, new_task)
else:
parent_element.append(new_task)
todo_manager._update_positions(parent_element if parent_element is not root else root)
# Save the file
todo_manager._save_tdl_file(tree, DEFAULT_TDL_FILE)
return f"Successfully added task '{task_data['title']}' at position {new_task.get('POSSTRING')}"
except Exception as e:
return f"Error adding task: {str(e)}"
@mcp.tool()
def update_task(args: UpdateTaskArgs) -> str:
"""Update an existing task in your main ToDoList file"""
if not Path(DEFAULT_TDL_FILE).exists():
return f"Error: ToDoList file doesn't exist: {DEFAULT_TDL_FILE}. Please open ToDoList first to create the file."
try:
# Convert args to dict and filter out None values for optional parameters
update_data = {k: v for k, v in args.dict().items() if k != 'task_id' and v is not None}
# Call the update method
success, message = todo_manager.update_task(args.task_id, **update_data)
return message
except Exception as e:
return f"Error updating task: {str(e)}"
@mcp.tool()
def search_tasks(args: SearchTasksArgs) -> str:
"""Search and filter tasks in your main ToDoList file"""
if not Path(DEFAULT_TDL_FILE).exists():
return f"Your main todolist file doesn't exist: {DEFAULT_TDL_FILE}"
try:
tree = todo_manager.parse_tdl_file()
all_tasks = todo_manager.extract_tasks(tree)
# Create filter dict from args, excluding format and None values
filters = {k: v for k, v in args.dict().items() if k != 'format' and v is not None}
# Search tasks
filtered_tasks = todo_manager.search_tasks(all_tasks, **filters)
if not filtered_tasks:
return "No tasks found matching the search criteria."
if args.format == "json":
json_tasks = [{k: v for k, v in task.items() if k != 'xml_element'} for task in filtered_tasks]
return json.dumps(json_tasks, indent=2)
else:
result = f"# Search Results ({len(filtered_tasks)} tasks found)\n\n"
if filters:
filter_desc = []
for key, value in filters.items():
filter_desc.append(f"{key.replace('_', ' ')}: {value}")
result += f"**Filters:** {', '.join(filter_desc)}\n\n"
result += todo_manager.format_tasks_as_markdown(filtered_tasks)
return result
except Exception as e:
return f"Error searching tasks: {str(e)}"
@mcp.tool()
def move_task(args: MoveTaskArgs) -> str:
"""Move a task to a new position in the hierarchy."""
if not Path(DEFAULT_TDL_FILE).exists():
return f"Error: ToDoList file doesn't exist: {DEFAULT_TDL_FILE}."
try:
tree = todo_manager.parse_tdl_file()
root = tree.getroot()
# Find the task to move
task_to_move = root.find(f".//TASK[@ID='{args.task_id}']")
if task_to_move is None:
return f"Error: Task with ID '{args.task_id}' not found."
# Find the new parent element
parts = args.new_position.split('.')
parent_pos_string = ".".join(parts[:-1])
insert_index = int(parts[-1]) - 1
new_parent = todo_manager._find_task_by_pos_string(root, parent_pos_string)
if new_parent is None:
if not parent_pos_string: # Top-level
new_parent = root
else:
return f"Error: Could not find new parent task at position {parent_pos_string}"
# Detach from old parent
old_parent = root.find(f".//TASK/TASK[@ID='{args.task_id}']/..")
if old_parent is None:
old_parent = root
old_parent.remove(task_to_move)
todo_manager._update_positions(old_parent)
# Attach to new parent
new_parent.insert(insert_index, task_to_move)
todo_manager._update_positions(new_parent)
todo_manager._save_tdl_file(tree, DEFAULT_TDL_FILE)
return f"Successfully moved task {args.task_id} to position {task_to_move.get('POSSTRING')}"
except Exception as e:
return f"Error moving task: {str(e)}"
@mcp.tool()
def get_task(args: GetTaskArgs) -> str:
"""Get a specific task and its children from your main ToDoList file"""
if not Path(DEFAULT_TDL_FILE).exists():
return f"Your main todolist file doesn't exist: {DEFAULT_TDL_FILE}"
try:
tree = todo_manager.parse_tdl_file()
root = tree.getroot()
task_elem = root.find(f".//TASK[@ID='{args.task_id}']")
if task_elem is None:
return f"Error: Task with ID '{args.task_id}' not found."
task = todo_manager._extract_tasks_recursive(task_elem.find(".."))
task = [t for t in task if t['id'] == args.task_id]
if args.format == "json":
return json.dumps(task, indent=2)
else:
return f"# Task Details\n\n" + todo_manager.format_tasks_as_markdown(task)
except Exception as e:
return f"Error getting task: {str(e)}"
@mcp.tool()
def get_file_status() -> str:
"""Check the status of your main ToDoList file"""
file_path = Path(DEFAULT_TDL_FILE)
if not file_path.exists():
return f"Your main todolist file doesn't exist: {DEFAULT_TDL_FILE}"
stat = file_path.stat()
modified = datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S')
size = stat.st_size
try:
tree = todo_manager.parse_tdl_file()
root = tree.getroot()
tasks = todo_manager.extract_tasks(tree)
# Get ToDoList file info
project_name = root.get('PROJECTNAME', 'Unknown')
file_version = root.get('FILEVERSION', 'Unknown')
app_version = root.get('APPVER', 'Unknown')
task_count = len(tasks)
completed = len([t for t in tasks if t.get('completed', False)])
return f"""ToDoList File Status
File: {DEFAULT_TDL_FILE}
Project: {project_name}
Size: {size} bytes
Last Modified: {modified}
File Version: {file_version}
App Version: {app_version}
Total Tasks: {task_count}
Completed: {completed}
Remaining: {task_count - completed}"""
except Exception as e:
return f"""ToDoList File Status
File: {DEFAULT_TDL_FILE}
Size: {size} bytes
Last Modified: {modified}
Warning: Error reading tasks: {str(e)}"""
class ReadAnyTdlArgs(BaseModel):
file_path: str = Field(..., description="Path to the .tdl file")
format: Literal['markdown', 'json'] = Field("markdown", description="Output format")
@mcp.tool()
def read_any_tdl_file(args: ReadAnyTdlArgs) -> str:
"""Read tasks from any ToDoList .tdl file"""
try:
tree = todo_manager.parse_tdl_file(args.file_path)
tasks = todo_manager.extract_tasks(tree)
if args.format == "json":
json_tasks = [{k: v for k, v in task.items() if k != 'xml_element'} for task in tasks]
return json.dumps(json_tasks, indent=2)
else:
return f"# Tasks from {args.file_path}\n\n" + todo_manager.format_tasks_as_markdown(tasks)
except Exception as e:
return f"Error reading file {args.file_path}: {str(e)}"
@mcp.tool()
def analyze_structure() -> str:
"""Analyze the XML structure of your main ToDoList file"""
if not Path(DEFAULT_TDL_FILE).exists():
return f"Your main todolist file doesn't exist yet: {DEFAULT_TDL_FILE}"
try:
tree = todo_manager.parse_tdl_file()
root = tree.getroot()
# Show ToDoList specific structure
info = []
info.append(f"Root Element: {root.tag}")
info.append(f"Project Name: {root.get('PROJECTNAME', 'Unknown')}")
info.append(f"File Version: {root.get('FILEVERSION', 'Unknown')}")
info.append(f"App Version: {root.get('APPVER', 'Unknown')}")
info.append(f"Next Unique ID: {root.get('NEXTUNIQUEID', 'Unknown')}")
# Count different elements
tasks = root.findall('.//TASK')
categories = root.findall('.//CATEGORY')
statuses = root.findall('.//STATUS')
info.append(f"\nElement Counts:")
info.append(f"Tasks: {len(tasks)}")
info.append(f"Categories: {len(set(cat.text for cat in categories if cat.text))}")
info.append(f"Statuses: {len(set(stat.text for stat in statuses if stat.text))}")
# Show task attributes structure
if tasks:
sample_task = tasks[0]
info.append(f"\nSample Task Attributes:")
for attr, value in sample_task.attrib.items():
info.append(f" {attr}: {value[:50]}..." if len(value) > 50 else f" {attr}: {value}")
return "\n".join(info)
except Exception as e:
return f"Error analyzing {DEFAULT_TDL_FILE}: {str(e)}"
if __name__ == "__main__":
mcp.run()