docs: add implementation plan and clarify no-file-loaded errors

Plan artifacts:
- plan.md: Technical context, constitution check, project structure
- research.md: XER format, MCP SDK, SQLite schema decisions
- data-model.md: Entity definitions and database schema
- contracts/mcp-tools.json: MCP tool schemas (9 tools)
- quickstart.md: Usage guide with examples
- CLAUDE.md: Agent context file

Spec updates:
- Add FR-015: NO_FILE_LOADED error requirement
- Add acceptance scenarios for no-file-loaded errors to US3, US4
This commit is contained in:
2026-01-06 20:57:55 -05:00
parent 0170eb7fef
commit d3474b0f8b
7 changed files with 1229 additions and 0 deletions

29
CLAUDE.md Normal file
View File

@@ -0,0 +1,29 @@
# xer-mcp Development Guidelines
Auto-generated from all feature plans. Last updated: 2026-01-06
## Active Technologies
- Python 3.14 + mcp (MCP SDK), sqlite3 (stdlib) (001-schedule-tools)
## Project Structure
```text
src/
tests/
```
## Commands
cd src [ONLY COMMANDS FOR ACTIVE TECHNOLOGIES][ONLY COMMANDS FOR ACTIVE TECHNOLOGIES] pytest [ONLY COMMANDS FOR ACTIVE TECHNOLOGIES][ONLY COMMANDS FOR ACTIVE TECHNOLOGIES] ruff check .
## Code Style
Python 3.14: Follow standard conventions
## Recent Changes
- 001-schedule-tools: Added Python 3.14 + mcp (MCP SDK), sqlite3 (stdlib)
<!-- MANUAL ADDITIONS START -->
<!-- MANUAL ADDITIONS END -->

View File

@@ -0,0 +1,365 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "XER MCP Server Tools",
"description": "MCP tool definitions for Primavera P6 XER file analysis",
"version": "0.1.0",
"tools": [
{
"name": "load_xer",
"description": "Load a Primavera P6 XER file and parse its schedule data. For multi-project files, specify project_id to select a project.",
"inputSchema": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Absolute path to the XER file"
},
"project_id": {
"type": "string",
"description": "Project ID to select (required for multi-project files)"
}
},
"required": ["file_path"]
},
"outputSchema": {
"type": "object",
"properties": {
"success": { "type": "boolean" },
"project": {
"type": "object",
"properties": {
"proj_id": { "type": "string" },
"proj_short_name": { "type": "string" },
"plan_start_date": { "type": "string", "format": "date-time" },
"plan_end_date": { "type": "string", "format": "date-time" }
}
},
"activity_count": { "type": "integer" },
"relationship_count": { "type": "integer" },
"available_projects": {
"type": "array",
"items": {
"type": "object",
"properties": {
"proj_id": { "type": "string" },
"proj_short_name": { "type": "string" }
}
},
"description": "Only present for multi-project files without selection"
},
"warnings": {
"type": "array",
"items": { "type": "string" }
}
}
}
},
{
"name": "list_activities",
"description": "List activities from the loaded XER file with optional filtering and pagination.",
"inputSchema": {
"type": "object",
"properties": {
"start_date": {
"type": "string",
"format": "date",
"description": "Filter activities starting on or after this date (YYYY-MM-DD)"
},
"end_date": {
"type": "string",
"format": "date",
"description": "Filter activities ending on or before this date (YYYY-MM-DD)"
},
"wbs_id": {
"type": "string",
"description": "Filter by WBS element ID"
},
"activity_type": {
"type": "string",
"enum": ["TT_Task", "TT_Mile", "TT_LOE", "TT_WBS", "TT_Rsrc"],
"description": "Filter by activity type"
},
"limit": {
"type": "integer",
"default": 100,
"minimum": 1,
"maximum": 1000,
"description": "Maximum number of activities to return"
},
"offset": {
"type": "integer",
"default": 0,
"minimum": 0,
"description": "Number of activities to skip"
}
}
},
"outputSchema": {
"type": "object",
"properties": {
"activities": {
"type": "array",
"items": {
"$ref": "#/$defs/ActivitySummary"
}
},
"pagination": { "$ref": "#/$defs/Pagination" }
}
}
},
{
"name": "get_activity",
"description": "Get detailed information for a specific activity by ID.",
"inputSchema": {
"type": "object",
"properties": {
"activity_id": {
"type": "string",
"description": "The task_id of the activity"
}
},
"required": ["activity_id"]
},
"outputSchema": {
"$ref": "#/$defs/ActivityDetail"
}
},
{
"name": "list_relationships",
"description": "List all relationships (dependencies) with pagination.",
"inputSchema": {
"type": "object",
"properties": {
"limit": {
"type": "integer",
"default": 100,
"minimum": 1,
"maximum": 1000
},
"offset": {
"type": "integer",
"default": 0,
"minimum": 0
}
}
},
"outputSchema": {
"type": "object",
"properties": {
"relationships": {
"type": "array",
"items": { "$ref": "#/$defs/Relationship" }
},
"pagination": { "$ref": "#/$defs/Pagination" }
}
}
},
{
"name": "get_predecessors",
"description": "Get all predecessor activities for a given activity.",
"inputSchema": {
"type": "object",
"properties": {
"activity_id": {
"type": "string",
"description": "The task_id of the activity"
}
},
"required": ["activity_id"]
},
"outputSchema": {
"type": "object",
"properties": {
"activity_id": { "type": "string" },
"predecessors": {
"type": "array",
"items": { "$ref": "#/$defs/RelatedActivity" }
}
}
}
},
{
"name": "get_successors",
"description": "Get all successor activities for a given activity.",
"inputSchema": {
"type": "object",
"properties": {
"activity_id": {
"type": "string",
"description": "The task_id of the activity"
}
},
"required": ["activity_id"]
},
"outputSchema": {
"type": "object",
"properties": {
"activity_id": { "type": "string" },
"successors": {
"type": "array",
"items": { "$ref": "#/$defs/RelatedActivity" }
}
}
}
},
{
"name": "get_project_summary",
"description": "Get high-level summary of the loaded project.",
"inputSchema": {
"type": "object",
"properties": {}
},
"outputSchema": {
"type": "object",
"properties": {
"project": {
"type": "object",
"properties": {
"proj_id": { "type": "string" },
"proj_short_name": { "type": "string" },
"plan_start_date": { "type": "string", "format": "date-time" },
"plan_end_date": { "type": "string", "format": "date-time" }
}
},
"activity_count": { "type": "integer" },
"milestone_count": { "type": "integer" },
"relationship_count": { "type": "integer" },
"wbs_element_count": { "type": "integer" },
"critical_activity_count": { "type": "integer" }
}
}
},
{
"name": "list_milestones",
"description": "List all milestone activities with pagination.",
"inputSchema": {
"type": "object",
"properties": {
"limit": {
"type": "integer",
"default": 100,
"minimum": 1,
"maximum": 1000
},
"offset": {
"type": "integer",
"default": 0,
"minimum": 0
}
}
},
"outputSchema": {
"type": "object",
"properties": {
"milestones": {
"type": "array",
"items": { "$ref": "#/$defs/ActivitySummary" }
},
"pagination": { "$ref": "#/$defs/Pagination" }
}
}
},
{
"name": "get_critical_path",
"description": "Get activities on the critical path (using P6's stored critical flags).",
"inputSchema": {
"type": "object",
"properties": {
"limit": {
"type": "integer",
"default": 100,
"minimum": 1,
"maximum": 1000
},
"offset": {
"type": "integer",
"default": 0,
"minimum": 0
}
}
},
"outputSchema": {
"type": "object",
"properties": {
"critical_activities": {
"type": "array",
"items": { "$ref": "#/$defs/ActivitySummary" }
},
"pagination": { "$ref": "#/$defs/Pagination" }
}
}
}
],
"$defs": {
"ActivitySummary": {
"type": "object",
"properties": {
"task_id": { "type": "string" },
"task_code": { "type": "string" },
"task_name": { "type": "string" },
"task_type": { "type": "string" },
"target_start_date": { "type": "string", "format": "date-time" },
"target_end_date": { "type": "string", "format": "date-time" },
"status_code": { "type": "string" },
"driving_path_flag": { "type": "boolean" }
}
},
"ActivityDetail": {
"type": "object",
"properties": {
"task_id": { "type": "string" },
"task_code": { "type": "string" },
"task_name": { "type": "string" },
"task_type": { "type": "string" },
"wbs_id": { "type": "string" },
"wbs_name": { "type": "string" },
"target_start_date": { "type": "string", "format": "date-time" },
"target_end_date": { "type": "string", "format": "date-time" },
"act_start_date": { "type": "string", "format": "date-time" },
"act_end_date": { "type": "string", "format": "date-time" },
"total_float_hr_cnt": { "type": "number" },
"status_code": { "type": "string" },
"driving_path_flag": { "type": "boolean" },
"predecessor_count": { "type": "integer" },
"successor_count": { "type": "integer" }
}
},
"Relationship": {
"type": "object",
"properties": {
"task_pred_id": { "type": "string" },
"task_id": { "type": "string" },
"task_name": { "type": "string" },
"pred_task_id": { "type": "string" },
"pred_task_name": { "type": "string" },
"pred_type": {
"type": "string",
"enum": ["FS", "SS", "FF", "SF"]
},
"lag_hr_cnt": { "type": "number" }
}
},
"RelatedActivity": {
"type": "object",
"properties": {
"task_id": { "type": "string" },
"task_code": { "type": "string" },
"task_name": { "type": "string" },
"relationship_type": {
"type": "string",
"enum": ["FS", "SS", "FF", "SF"]
},
"lag_hr_cnt": { "type": "number" }
}
},
"Pagination": {
"type": "object",
"properties": {
"total_count": { "type": "integer" },
"offset": { "type": "integer" },
"limit": { "type": "integer" },
"has_more": { "type": "boolean" }
}
}
}
}

View File

@@ -0,0 +1,234 @@
# Data Model: Project Schedule Tools
**Date**: 2026-01-06
**Branch**: `001-schedule-tools`
## Entity Overview
```
┌─────────────┐ ┌─────────────┐
│ Project │───────│ WBS │
└─────────────┘ └─────────────┘
│ │
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Activity │◄──────│ Relationship│
└─────────────┘ └─────────────┘
┌─────────────┐
│ Calendar │ (internal only)
└─────────────┘
```
## Entities
### Project
Top-level container representing a P6 project.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| proj_id | string | Yes | Unique project identifier from P6 |
| proj_short_name | string | Yes | Short display name |
| plan_start_date | datetime | No | Planned start date |
| plan_end_date | datetime | No | Planned end date |
| loaded_at | datetime | Yes | When file was loaded into server |
**XER Source**: `PROJECT` table
**Validation Rules**:
- proj_id must be unique within loaded data
- proj_short_name must not be empty
### Activity
A unit of work in the schedule.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| task_id | string | Yes | Unique activity identifier |
| proj_id | string | Yes | Parent project reference |
| wbs_id | string | No | WBS element reference |
| task_code | string | Yes | User-visible activity code |
| task_name | string | Yes | Activity description |
| task_type | enum | Yes | TT_Task, TT_Mile, TT_LOE, TT_WBS, TT_Rsrc |
| target_start_date | datetime | No | Planned start |
| target_end_date | datetime | No | Planned finish |
| act_start_date | datetime | No | Actual start |
| act_end_date | datetime | No | Actual finish |
| total_float_hr_cnt | float | No | Total float in hours |
| driving_path_flag | boolean | No | True if on critical path |
| status_code | enum | No | TK_NotStart, TK_Active, TK_Complete |
**XER Source**: `TASK` table
**Validation Rules**:
- task_id must be unique
- task_code must not be empty
- task_name must not be empty
- If act_start_date exists, target_start_date should exist
- If act_end_date exists, act_start_date must exist
**State Transitions**:
```
TK_NotStart ──[actual start recorded]──► TK_Active
TK_Active ──[actual finish recorded]──► TK_Complete
```
### Relationship
A dependency link between two activities.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| task_pred_id | string | Yes | Unique relationship identifier |
| task_id | string | Yes | Successor activity (the one being constrained) |
| pred_task_id | string | Yes | Predecessor activity (the one constraining) |
| pred_type | enum | Yes | PR_FS, PR_SS, PR_FF, PR_SF |
| lag_hr_cnt | float | No | Lag time in hours (can be negative) |
**XER Source**: `TASKPRED` table
**Relationship Types**:
| Code | Name | Meaning |
|------|------|---------|
| PR_FS | Finish-to-Start | Successor starts after predecessor finishes |
| PR_SS | Start-to-Start | Successor starts after predecessor starts |
| PR_FF | Finish-to-Finish | Successor finishes after predecessor finishes |
| PR_SF | Start-to-Finish | Successor finishes after predecessor starts |
**Validation Rules**:
- task_id and pred_task_id must reference existing activities
- task_id must not equal pred_task_id (no self-reference)
- pred_type must be one of the four valid types
### WBS (Work Breakdown Structure)
Hierarchical organization of activities.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| wbs_id | string | Yes | Unique WBS identifier |
| proj_id | string | Yes | Parent project reference |
| parent_wbs_id | string | No | Parent WBS element (null for root) |
| wbs_short_name | string | Yes | Short code |
| wbs_name | string | No | Full description |
| wbs_level | integer | No | Hierarchy depth (0 = root) |
**XER Source**: `PROJWBS` table
**Validation Rules**:
- wbs_id must be unique
- parent_wbs_id must reference existing WBS or be null
- No circular parent references
### Calendar (Internal)
Work schedule definition. Not exposed via MCP tools.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| clndr_id | string | Yes | Unique calendar identifier |
| clndr_name | string | Yes | Calendar name |
| day_hr_cnt | float | No | Hours per work day |
| week_hr_cnt | float | No | Hours per work week |
**XER Source**: `CALENDAR` table
**Note**: Parsed and stored but not exposed as queryable. Used internally if duration calculations are needed in future.
### PaginationMetadata
Response wrapper for paginated queries.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| total_count | integer | Yes | Total items matching query |
| offset | integer | Yes | Current offset (0-based) |
| limit | integer | Yes | Items per page |
| has_more | boolean | Yes | True if more items exist |
## SQLite Schema
```sql
-- Projects
CREATE TABLE projects (
proj_id TEXT PRIMARY KEY,
proj_short_name TEXT NOT NULL,
plan_start_date TEXT,
plan_end_date TEXT,
loaded_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Activities
CREATE TABLE activities (
task_id TEXT PRIMARY KEY,
proj_id TEXT NOT NULL,
wbs_id TEXT,
task_code TEXT NOT NULL,
task_name TEXT NOT NULL,
task_type TEXT NOT NULL,
target_start_date TEXT,
target_end_date TEXT,
act_start_date TEXT,
act_end_date TEXT,
total_float_hr_cnt REAL,
driving_path_flag INTEGER DEFAULT 0,
status_code TEXT,
FOREIGN KEY (proj_id) REFERENCES projects(proj_id),
FOREIGN KEY (wbs_id) REFERENCES wbs(wbs_id)
);
-- Relationships
CREATE TABLE relationships (
task_pred_id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
pred_task_id TEXT NOT NULL,
pred_type TEXT NOT NULL,
lag_hr_cnt REAL DEFAULT 0,
FOREIGN KEY (task_id) REFERENCES activities(task_id),
FOREIGN KEY (pred_task_id) REFERENCES activities(task_id)
);
-- WBS
CREATE TABLE wbs (
wbs_id TEXT PRIMARY KEY,
proj_id TEXT NOT NULL,
parent_wbs_id TEXT,
wbs_short_name TEXT NOT NULL,
wbs_name TEXT,
FOREIGN KEY (proj_id) REFERENCES projects(proj_id),
FOREIGN KEY (parent_wbs_id) REFERENCES wbs(wbs_id)
);
-- Calendars (internal)
CREATE TABLE calendars (
clndr_id TEXT PRIMARY KEY,
clndr_name TEXT NOT NULL,
day_hr_cnt REAL,
week_hr_cnt REAL
);
-- Indexes
CREATE INDEX idx_activities_proj ON activities(proj_id);
CREATE INDEX idx_activities_wbs ON activities(wbs_id);
CREATE INDEX idx_activities_type ON activities(task_type);
CREATE INDEX idx_activities_critical ON activities(driving_path_flag) WHERE driving_path_flag = 1;
CREATE INDEX idx_activities_dates ON activities(target_start_date, target_end_date);
CREATE INDEX idx_relationships_task ON relationships(task_id);
CREATE INDEX idx_relationships_pred ON relationships(pred_task_id);
CREATE INDEX idx_wbs_parent ON wbs(parent_wbs_id);
CREATE INDEX idx_wbs_proj ON wbs(proj_id);
```
## Date Handling
All dates stored as ISO8601 strings in SQLite for portability:
- Format: `YYYY-MM-DDTHH:MM:SS`
- Timezone: Preserved from XER if present, otherwise naive
- Comparisons: String comparison works correctly with ISO8601
XER date format: `YYYY-MM-DD HH:MM` (space-separated, no seconds)
Conversion on import: Add `:00` for seconds, replace space with `T`

View File

@@ -0,0 +1,127 @@
# Implementation Plan: Project Schedule Tools
**Branch**: `001-schedule-tools` | **Date**: 2026-01-06 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/001-schedule-tools/spec.md`
## Summary
Build an MCP server that parses Primavera P6 XER files and exposes schedule data (activities, relationships, project summaries) through MCP tools. The server uses stdio transport, loads XER data into SQLite for efficient querying, and implements pagination to prevent context overflow for AI assistants.
## Technical Context
**Language/Version**: Python 3.14
**Package Manager**: uv
**Primary Dependencies**: mcp (MCP SDK), sqlite3 (stdlib)
**Storage**: SQLite (in-memory or file-based per session)
**Testing**: pytest with pytest-asyncio for async MCP handlers
**Target Platform**: Linux/macOS/Windows (cross-platform CLI)
**Transport**: stdio (standard input/output)
**Project Type**: Single project
**Performance Goals**: Load 10,000 activities in <5 seconds; query response <1 second
**Constraints**: Default 100-item pagination limit; single-user operation
**Scale/Scope**: Files up to 50,000 activities
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Principle | Requirement | Status | Notes |
|-----------|-------------|--------|-------|
| I. Test-First Development | TDD mandatory; tests fail before implementation | ✅ PASS | Plan includes contract tests for MCP tools, integration tests for XER parsing |
| II. Extensibility Architecture | Pluggable handlers; separated concerns | ✅ PASS | XER parser separated from MCP layer; table handlers pluggable |
| III. MCP Protocol Compliance | Complete JSON schemas; proper error format | ✅ PASS | All tools will have full schemas; errors follow MCP format |
| IV. XER Format Fidelity | No data loss; preserve precision | ✅ PASS | SQLite preserves all parsed data; unknown tables stored |
| V. Semantic Versioning | SemVer for releases | ✅ PASS | Will use 0.1.0 for initial release |
**Technical Standards Compliance**:
- Python 3.14 ✅
- Type hints throughout ✅
- Formatting via ruff ✅
- Dependencies pinned in pyproject.toml ✅
- Console logging ✅
## Project Structure
### Documentation (this feature)
```text
specs/001-schedule-tools/
├── plan.md # This file
├── research.md # Phase 0 output
├── data-model.md # Phase 1 output
├── quickstart.md # Phase 1 output
├── contracts/ # Phase 1 output (MCP tool schemas)
└── tasks.md # Phase 2 output (/speckit.tasks command)
```
### Source Code (repository root)
```text
src/
├── xer_mcp/
│ ├── __init__.py
│ ├── server.py # MCP server entry point (stdio)
│ ├── tools/ # MCP tool implementations
│ │ ├── __init__.py
│ │ ├── load_xer.py
│ │ ├── list_activities.py
│ │ ├── get_activity.py
│ │ ├── list_relationships.py
│ │ ├── get_predecessors.py
│ │ ├── get_successors.py
│ │ ├── get_project_summary.py
│ │ ├── list_milestones.py
│ │ └── get_critical_path.py
│ ├── parser/ # XER file parsing
│ │ ├── __init__.py
│ │ ├── xer_parser.py # Main parser
│ │ └── table_handlers/ # Pluggable table handlers
│ │ ├── __init__.py
│ │ ├── base.py # Abstract base class
│ │ ├── project.py
│ │ ├── task.py
│ │ ├── taskpred.py
│ │ ├── projwbs.py
│ │ └── calendar.py
│ ├── db/ # SQLite database layer
│ │ ├── __init__.py
│ │ ├── schema.py # Table definitions
│ │ ├── loader.py # Load parsed data into SQLite
│ │ └── queries.py # Query functions with pagination
│ └── models/ # Data models (dataclasses)
│ ├── __init__.py
│ ├── project.py
│ ├── activity.py
│ ├── relationship.py
│ └── pagination.py
tests/
├── conftest.py # Shared fixtures (sample XER files)
├── contract/ # MCP tool contract tests
│ ├── test_load_xer.py
│ ├── test_list_activities.py
│ ├── test_get_activity.py
│ ├── test_list_relationships.py
│ ├── test_get_predecessors.py
│ ├── test_get_successors.py
│ ├── test_get_project_summary.py
│ ├── test_list_milestones.py
│ └── test_get_critical_path.py
├── integration/ # End-to-end XER parsing tests
│ ├── test_xer_parsing.py
│ ├── test_multi_project.py
│ └── test_edge_cases.py
└── unit/ # Unit tests for parser, db, models
├── test_parser.py
├── test_table_handlers.py
├── test_db_queries.py
└── test_models.py
pyproject.toml # Project config with uv
```
**Structure Decision**: Single project structure with clear separation between MCP tools, XER parsing, and SQLite database layers. The parser/table_handlers/ directory enables extensibility for future XER table types.
## Complexity Tracking
No constitution violations requiring justification. Design follows all principles.

View File

@@ -0,0 +1,270 @@
# Quickstart: XER MCP Server
This guide shows how to set up and use the XER MCP Server for querying Primavera P6 schedule data.
## Prerequisites
- Python 3.14+
- uv package manager
- An XER file exported from Primavera P6
## Installation
```bash
# Clone the repository
git clone <repo-url>
cd xer-mcp
# Install dependencies with uv
uv sync
# Verify installation
uv run python -m xer_mcp --help
```
## Configuration
Add the server to your MCP client configuration (e.g., Claude Desktop):
```json
{
"mcpServers": {
"xer-mcp": {
"command": "uv",
"args": ["run", "python", "-m", "xer_mcp"],
"cwd": "/path/to/xer-mcp"
}
}
}
```
## Basic Usage
### 1. Load an XER File
```
Use the load_xer tool with file_path="/path/to/schedule.xer"
```
**Response** (single project):
```json
{
"success": true,
"project": {
"proj_id": "P001",
"proj_short_name": "Construction Phase 1",
"plan_start_date": "2026-01-15T00:00:00",
"plan_end_date": "2026-06-30T00:00:00"
},
"activity_count": 450,
"relationship_count": 520
}
```
**Response** (multi-project, no selection):
```json
{
"success": false,
"available_projects": [
{"proj_id": "P001", "proj_short_name": "Phase 1"},
{"proj_id": "P002", "proj_short_name": "Phase 2"}
],
"message": "Multiple projects found. Please specify project_id."
}
```
### 2. List Activities
```
Use the list_activities tool
```
**Response**:
```json
{
"activities": [
{
"task_id": "A1000",
"task_code": "A1000",
"task_name": "Site Preparation",
"task_type": "TT_Task",
"target_start_date": "2026-01-15T08:00:00",
"target_end_date": "2026-01-22T17:00:00",
"status_code": "TK_NotStart",
"driving_path_flag": true
}
],
"pagination": {
"total_count": 450,
"offset": 0,
"limit": 100,
"has_more": true
}
}
```
### 3. Filter Activities by Date Range
```
Use the list_activities tool with start_date="2026-02-01" and end_date="2026-02-28"
```
### 4. Get Activity Details
```
Use the get_activity tool with activity_id="A1000"
```
**Response**:
```json
{
"task_id": "A1000",
"task_code": "A1000",
"task_name": "Site Preparation",
"task_type": "TT_Task",
"wbs_id": "WBS001",
"wbs_name": "Pre-Construction",
"target_start_date": "2026-01-15T08:00:00",
"target_end_date": "2026-01-22T17:00:00",
"act_start_date": null,
"act_end_date": null,
"total_float_hr_cnt": 0,
"status_code": "TK_NotStart",
"driving_path_flag": true,
"predecessor_count": 0,
"successor_count": 5
}
```
### 5. Query Predecessors/Successors
```
Use the get_predecessors tool with activity_id="A1050"
```
**Response**:
```json
{
"activity_id": "A1050",
"predecessors": [
{
"task_id": "A1000",
"task_code": "A1000",
"task_name": "Site Preparation",
"relationship_type": "FS",
"lag_hr_cnt": 0
},
{
"task_id": "A1010",
"task_code": "A1010",
"task_name": "Permits Approved",
"relationship_type": "FS",
"lag_hr_cnt": 8
}
]
}
```
### 6. Get Project Summary
```
Use the get_project_summary tool
```
**Response**:
```json
{
"project": {
"proj_id": "P001",
"proj_short_name": "Construction Phase 1",
"plan_start_date": "2026-01-15T00:00:00",
"plan_end_date": "2026-06-30T00:00:00"
},
"activity_count": 450,
"milestone_count": 25,
"relationship_count": 520,
"wbs_element_count": 30,
"critical_activity_count": 85
}
```
### 7. Get Critical Path
```
Use the get_critical_path tool
```
**Response**:
```json
{
"critical_activities": [
{
"task_id": "A1000",
"task_code": "A1000",
"task_name": "Site Preparation",
"task_type": "TT_Task",
"target_start_date": "2026-01-15T08:00:00",
"target_end_date": "2026-01-22T17:00:00"
}
],
"pagination": {
"total_count": 85,
"offset": 0,
"limit": 100,
"has_more": false
}
}
```
### 8. List Milestones
```
Use the list_milestones tool
```
## Pagination
All list operations support pagination:
```
Use the list_activities tool with limit=50 and offset=100
```
This returns activities 101-150 (0-indexed).
## Error Handling
| Error | Meaning | Solution |
|-------|---------|----------|
| FILE_NOT_FOUND | XER file path doesn't exist | Check file path |
| PARSE_ERROR | XER file is malformed | Verify file is valid P6 export |
| NO_FILE_LOADED | Query before load_xer | Call load_xer first |
| PROJECT_SELECTION_REQUIRED | Multi-project file | Specify project_id in load_xer |
| ACTIVITY_NOT_FOUND | Invalid activity_id | Check activity exists |
## Relationship Types
| Code | Name | Meaning |
|------|------|---------|
| FS | Finish-to-Start | B starts after A finishes |
| SS | Start-to-Start | B starts after A starts |
| FF | Finish-to-Finish | B finishes after A finishes |
| SF | Start-to-Finish | B finishes after A starts |
## Activity Types
| Code | Name | Description |
|------|------|-------------|
| TT_Task | Task | Normal work activity with duration |
| TT_Mile | Milestone | Zero-duration marker |
| TT_LOE | Level of Effort | Duration spans linked activities |
| TT_WBS | WBS Summary | Summary of child activities |
| TT_Rsrc | Resource Dependent | Duration from resource assignment |
## Tips for AI Assistants
1. **Start with summary**: Call `get_project_summary` first to understand scope
2. **Use pagination**: Large schedules may have thousands of activities
3. **Filter early**: Use date ranges and WBS filters to reduce results
4. **Check critical path**: `get_critical_path` identifies schedule-driving work
5. **Follow relationships**: Use `get_predecessors`/`get_successors` to trace dependencies

View File

@@ -0,0 +1,200 @@
# Research: Project Schedule Tools
**Date**: 2026-01-06
**Branch**: `001-schedule-tools`
## XER File Format
### Decision: Parse tab-delimited format with %T table headers
**Rationale**: XER is Primavera P6's native export format. It uses a simple text-based structure that's straightforward to parse without external libraries.
**Format Structure**:
```
ERMHDR ...header info...
%T TABLE_NAME
%F field1 field2 field3 ...
%R value1 value2 value3 ...
%R value1 value2 value3 ...
%T NEXT_TABLE
...
%E
```
**Key Tables for Schedule Tools**:
| Table | Purpose | Key Fields |
|-------|---------|------------|
| PROJECT | Project metadata | proj_id, proj_short_name, plan_start_date, plan_end_date |
| TASK | Activities | task_id, task_code, task_name, task_type, target_start_date, target_end_date, act_start_date, act_end_date, driving_path_flag |
| TASKPRED | Relationships | task_pred_id, task_id, pred_task_id, pred_type, lag_hr_cnt |
| PROJWBS | WBS structure | wbs_id, wbs_short_name, wbs_name, parent_wbs_id, proj_id |
| CALENDAR | Work calendars | clndr_id, clndr_name, day_hr_cnt |
**Alternatives Considered**:
- XML export: More complex to parse, larger file sizes
- Database direct access: Requires P6 installation, not portable
## MCP Python SDK
### Decision: Use `mcp` package with stdio transport
**Rationale**: The official MCP Python SDK provides a clean async interface for building MCP servers. Stdio transport is simplest for local tools.
**Implementation Pattern**:
```python
from mcp.server import Server
from mcp.server.stdio import stdio_server
server = Server("xer-mcp")
@server.tool()
async def load_xer(file_path: str, project_id: str | None = None) -> dict:
"""Load an XER file and optionally select a project."""
...
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream, server.create_initialization_options())
```
**Key Considerations**:
- All tools are async functions decorated with `@server.tool()`
- Tool parameters use Python type hints for JSON schema generation
- Return values are automatically serialized to JSON
- Errors should raise `McpError` with appropriate error codes
**Alternatives Considered**:
- SSE transport: Adds complexity, not needed for local use
- Custom protocol: Would break MCP compatibility
## SQLite Schema Design
### Decision: In-memory SQLite with normalized tables
**Rationale**: SQLite in-memory mode provides fast queries without file I/O overhead. Normalized tables map directly to XER structure while enabling efficient JOINs for relationship queries.
**Schema Design**:
```sql
-- Project table
CREATE TABLE projects (
proj_id TEXT PRIMARY KEY,
proj_short_name TEXT NOT NULL,
plan_start_date TEXT, -- ISO8601
plan_end_date TEXT,
loaded_at TEXT NOT NULL
);
-- Activities table
CREATE TABLE activities (
task_id TEXT PRIMARY KEY,
proj_id TEXT NOT NULL REFERENCES projects(proj_id),
wbs_id TEXT,
task_code TEXT NOT NULL,
task_name TEXT NOT NULL,
task_type TEXT, -- TT_Task, TT_Mile, TT_LOE, etc.
target_start_date TEXT,
target_end_date TEXT,
act_start_date TEXT,
act_end_date TEXT,
total_float_hr_cnt REAL,
driving_path_flag TEXT, -- 'Y' or 'N'
status_code TEXT
);
-- Relationships table
CREATE TABLE relationships (
task_pred_id TEXT PRIMARY KEY,
task_id TEXT NOT NULL REFERENCES activities(task_id),
pred_task_id TEXT NOT NULL REFERENCES activities(task_id),
pred_type TEXT NOT NULL, -- PR_FS, PR_SS, PR_FF, PR_SF
lag_hr_cnt REAL DEFAULT 0
);
-- WBS table
CREATE TABLE wbs (
wbs_id TEXT PRIMARY KEY,
proj_id TEXT NOT NULL REFERENCES projects(proj_id),
parent_wbs_id TEXT REFERENCES wbs(wbs_id),
wbs_short_name TEXT NOT NULL,
wbs_name TEXT
);
-- Indexes for common queries
CREATE INDEX idx_activities_proj ON activities(proj_id);
CREATE INDEX idx_activities_wbs ON activities(wbs_id);
CREATE INDEX idx_activities_type ON activities(task_type);
CREATE INDEX idx_activities_dates ON activities(target_start_date, target_end_date);
CREATE INDEX idx_relationships_task ON relationships(task_id);
CREATE INDEX idx_relationships_pred ON relationships(pred_task_id);
CREATE INDEX idx_wbs_parent ON wbs(parent_wbs_id);
```
**Query Patterns**:
- Pagination: `LIMIT ? OFFSET ?` with `COUNT(*)` for total
- Date filtering: `WHERE target_start_date >= ? AND target_end_date <= ?`
- Critical path: `WHERE driving_path_flag = 'Y'`
- Predecessors: `SELECT * FROM relationships WHERE task_id = ?`
- Successors: `SELECT * FROM relationships WHERE pred_task_id = ?`
**Alternatives Considered**:
- File-based SQLite: Adds complexity for file management, not needed for single-session use
- In-memory dictionaries: Would require custom indexing for efficient queries
- DuckDB: Overkill for this use case, larger dependency
## XER Parsing Strategy
### Decision: Streaming line-by-line parser with table handler registry
**Rationale**: XER files can be large (50K+ activities). Streaming avoids loading entire file into memory. Table handler registry enables extensibility per constitution.
**Implementation Approach**:
1. Read file line by line
2. Track current table context (%T lines)
3. Parse %F lines as field headers
4. Parse %R lines as records using current field map
5. Dispatch to registered table handler
6. Handler converts to model and inserts into SQLite
**Encoding Handling**:
- XER files use Windows-1252 encoding by default
- Attempt UTF-8 first, fallback to Windows-1252
- Log encoding detection result
## Pagination Implementation
### Decision: Offset-based pagination with metadata
**Rationale**: Simple to implement with SQLite's LIMIT/OFFSET. Metadata enables clients to navigate results.
**Response Format**:
```python
@dataclass
class PaginatedResponse:
items: list[dict]
pagination: PaginationMetadata
@dataclass
class PaginationMetadata:
total_count: int
offset: int
limit: int
has_more: bool
```
**Default Limit**: 100 items (per spec clarification)
## Error Handling
### Decision: Structured MCP errors with codes
**Rationale**: MCP protocol defines error format. Consistent error codes help clients handle failures.
**Error Codes**:
| Code | Name | When Used |
|------|------|-----------|
| -32001 | FILE_NOT_FOUND | XER file path doesn't exist |
| -32002 | PARSE_ERROR | XER file is malformed |
| -32003 | NO_FILE_LOADED | Query attempted before load |
| -32004 | PROJECT_SELECTION_REQUIRED | Multi-project file without selection |
| -32005 | ACTIVITY_NOT_FOUND | Requested activity ID doesn't exist |
| -32006 | INVALID_PARAMETER | Bad filter/pagination parameters |

View File

@@ -60,6 +60,7 @@ As an AI assistant user, I want to query the relationships (dependencies) betwee
3. **Given** an activity has no predecessors, **When** I request its predecessors, **Then** I receive an empty list (not an error)
4. **Given** an XER file is loaded, **When** I request all relationships, **Then** I receive the dependency network (limited to 100 by default) with pagination metadata
5. **Given** an XER file is loaded, **When** I request relationships with offset and limit parameters, **Then** I receive the specified page of results
6. **Given** no XER file is loaded, **When** I request relationships or predecessors/successors, **Then** I receive a clear error message indicating no file is loaded
---
@@ -76,6 +77,7 @@ As an AI assistant user, I want to get a high-level summary of the project sched
1. **Given** an XER file is loaded, **When** I request the project summary, **Then** I receive project name, start date, finish date, and total activity count
2. **Given** an XER file with milestones, **When** I request milestones, **Then** I receive a list of milestone activities with their target dates
3. **Given** an XER file is loaded, **When** I request the critical path, **Then** I receive the sequence of activities that determine the project end date
4. **Given** no XER file is loaded, **When** I request project summary, milestones, or critical path, **Then** I receive a clear error message indicating no file is loaded
---
@@ -105,6 +107,7 @@ As an AI assistant user, I want to get a high-level summary of the project sched
- **FR-012**: System MUST handle XER files with multiple projects by requiring explicit project selection; single-project files auto-select the only project
- **FR-013**: System MUST implement pagination for list queries with a default limit of 100 items, supporting offset and limit parameters
- **FR-014**: System MUST return pagination metadata (total_count, has_more, offset, limit) with all paginated responses
- **FR-015**: System MUST return an informative error (NO_FILE_LOADED) when any query tool is invoked before an XER file has been successfully loaded
### Key Entities
@@ -135,6 +138,7 @@ As an AI assistant user, I want to get a high-level summary of the project sched
- Q: How should critical path be determined? → A: Use P6's stored critical flags from XER data
- Q: How should multi-project XER files be handled? → A: Require explicit project selection if multiple exist
- Q: Should calendar data be exposed as queryable? → A: Internal use only (not exposed as queryable)
- Q: What happens when any query tool is called without a file loaded? → A: Return informative error indicating no XER file is loaded; applies to all tools except load_xer
## Assumptions