feat: implement XER MCP Server with 9 schedule query tools

Implement complete MCP server for parsing Primavera P6 XER files and
exposing schedule data through MCP tools. All 4 user stories complete.

Tools implemented:
- load_xer: Parse XER files into SQLite database
- list_activities: Query activities with pagination and filtering
- get_activity: Get activity details by ID
- list_relationships: Query activity dependencies
- get_predecessors/get_successors: Query activity relationships
- get_project_summary: Project overview with counts
- list_milestones: Query milestone activities
- get_critical_path: Query driving path activities

Features:
- Tab-delimited XER format parsing with pluggable table handlers
- In-memory SQLite database for fast queries
- Pagination with 100-item default limit
- Multi-project file support with project selection
- ISO8601 date formatting
- NO_FILE_LOADED error handling for all query tools

Test coverage: 81 tests (contract, integration, unit)
This commit is contained in:
2026-01-06 21:27:35 -05:00
parent 2cd54118a1
commit ccc8296418
56 changed files with 4140 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
"""TASK table handler."""
from xer_mcp.parser.table_handlers.base import TableHandler
from xer_mcp.parser.table_handlers.project import convert_date
class TaskHandler(TableHandler):
"""Handler for TASK table in XER files."""
@property
def table_name(self) -> str:
return "TASK"
def parse_row(self, fields: list[str], values: list[str]) -> dict | None:
"""Parse a TASK row."""
if len(values) < len(fields):
values = values + [""] * (len(fields) - len(values))
data = dict(zip(fields, values, strict=False))
# Parse driving_path_flag (Y/N -> bool)
driving_flag = data.get("driving_path_flag", "N")
driving_path = driving_flag.upper() == "Y" if driving_flag else False
# Parse total_float_hr_cnt
float_str = data.get("total_float_hr_cnt", "")
total_float = float(float_str) if float_str else None
return {
"task_id": data.get("task_id", ""),
"proj_id": data.get("proj_id", ""),
"wbs_id": data.get("wbs_id") or None,
"task_code": data.get("task_code", ""),
"task_name": data.get("task_name", ""),
"task_type": data.get("task_type", ""),
"status_code": data.get("status_code") or None,
"target_start_date": convert_date(data.get("target_start_date")),
"target_end_date": convert_date(data.get("target_end_date")),
"act_start_date": convert_date(data.get("act_start_date")),
"act_end_date": convert_date(data.get("act_end_date")),
"total_float_hr_cnt": total_float,
"driving_path_flag": driving_path,
}