Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ Update an existing work package.
- `status_id` (integer, optional): Status ID
- `priority_id` (integer, optional): Priority ID
- `assignee_id` (integer, optional): User ID to assign to
- `percentage_done` (integer, optional): Completion percentage (0-100)
- `percentage_done` (integer, optional): Completion percentage (0-100). **Known instance behavior:** on some OpenProject instances progress is derived from status and `percentageDone` is read-only — sending it returns HTTP 422 `PropertyIsReadOnly`. The tool detects this, skips `percentage_done`, and still applies the other fields (with a note in the response).

#### 13. `delete_work_package`
Delete a work package.
Expand Down
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ classifiers = [
]

dependencies = [
"fastmcp>=2.0.0",
"fastmcp>=2.0.0,<3", # 2.x tool API (@mcp.tool -> FunctionTool.fn); 3.x changes this
"mcp>=1.0.0",
"aiohttp>=3.8.0",
"python-dotenv>=1.0.0",
Expand All @@ -33,6 +33,7 @@ dependencies = [
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
"black>=22.0.0",
"flake8>=4.0.0",
]
Expand All @@ -51,3 +52,7 @@ target-version = ['py38']
[tool.flake8]
max-line-length = 88
extend-ignore = ["E203", "W503"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
7 changes: 4 additions & 3 deletions src/tools/memberships.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""Membership management tools."""

from typing import Optional, List
from pydantic import BaseModel, Field
from pydantic import Field
from src.server import mcp, get_client
from src.utils.inputs import CoercibleModel
from src.utils.formatting import format_success, format_error


class CreateMembershipInput(BaseModel):
class CreateMembershipInput(CoercibleModel):
"""Input model for creating memberships."""
project_id: int = Field(..., description="Project ID", gt=0)
user_id: Optional[int] = Field(None, description="User ID (required if group_id not provided)", gt=0)
Expand All @@ -16,7 +17,7 @@ class CreateMembershipInput(BaseModel):
notification_message: Optional[str] = Field(None, description="Optional notification message")


class UpdateMembershipInput(BaseModel):
class UpdateMembershipInput(CoercibleModel):
"""Input model for updating memberships."""
membership_id: int = Field(..., description="Membership ID to update", gt=0)
role_ids: Optional[List[int]] = Field(None, description="New list of role IDs")
Expand Down
7 changes: 4 additions & 3 deletions src/tools/news.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import json
from typing import Optional
from pydantic import BaseModel, Field
from pydantic import Field

from src.server import mcp, get_client
from src.utils.inputs import CoercibleModel
from src.utils.formatting import (
format_news_list,
format_news_detail,
Expand All @@ -18,7 +19,7 @@
# ============================================================


class CreateNewsInput(BaseModel):
class CreateNewsInput(CoercibleModel):
"""Input model for creating news."""

project_id: int = Field(..., description="Project ID", gt=0)
Expand All @@ -29,7 +30,7 @@ class CreateNewsInput(BaseModel):
description: str = Field(..., description="Main content (supports Markdown)")


class UpdateNewsInput(BaseModel):
class UpdateNewsInput(CoercibleModel):
"""Input model for updating news."""

news_id: int = Field(..., description="News ID to update", gt=0)
Expand Down
9 changes: 5 additions & 4 deletions src/tools/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import json
from typing import Optional
from src.server import mcp, get_client
from pydantic import BaseModel, Field
from pydantic import Field
from src.utils.inputs import CoercibleModel
from src.utils.formatting import format_success, format_error
from src.utils.formatting import format_project_list

Expand Down Expand Up @@ -140,7 +141,7 @@ async def get_project(project_id: int) -> str:
return f"❌ Failed to get project: {str(e)}"


class CreateProjectInput(BaseModel):
class CreateProjectInput(CoercibleModel):
"""Input model for creating projects."""
name: str = Field(..., description="Project name", min_length=1, max_length=255)
identifier: str = Field(..., description="Project identifier (lowercase, no spaces)", min_length=1, max_length=100)
Expand All @@ -150,7 +151,7 @@ class CreateProjectInput(BaseModel):
parent_id: Optional[int] = Field(None, description="Parent project ID for sub-projects", gt=0)


class AddSubprojectInput(BaseModel):
class AddSubprojectInput(CoercibleModel):
"""Input model for adding subprojects."""
parent_id: int = Field(..., description="Parent project ID", gt=0)
name: str = Field(..., description="Subproject name", min_length=1, max_length=255)
Expand All @@ -159,7 +160,7 @@ class AddSubprojectInput(BaseModel):
public: Optional[bool] = Field(None, description="Whether subproject is public")


class UpdateProjectInput(BaseModel):
class UpdateProjectInput(CoercibleModel):
"""Input model for updating projects."""
project_id: int = Field(..., description="Project ID to update", gt=0)
name: Optional[str] = Field(None, description="New project name", min_length=1, max_length=255)
Expand Down
7 changes: 4 additions & 3 deletions src/tools/relations.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""Work package relation management tools (follows, blocks, relates, etc.)."""

from typing import Optional
from pydantic import BaseModel, Field
from pydantic import Field
from src.server import mcp, get_client
from src.utils.inputs import CoercibleModel
from src.utils.formatting import format_success, format_error


class CreateRelationInput(BaseModel):
class CreateRelationInput(CoercibleModel):
"""Input model for creating work package relations."""
from_id: int = Field(..., description="Source work package ID", gt=0)
to_id: int = Field(..., description="Target work package ID", gt=0)
Expand All @@ -15,7 +16,7 @@ class CreateRelationInput(BaseModel):
description: Optional[str] = Field(None, description="Relation description")


class UpdateRelationInput(BaseModel):
class UpdateRelationInput(CoercibleModel):
"""Input model for updating work package relations."""
relation_id: int = Field(..., description="Relation ID to update", gt=0)
lag: Optional[int] = Field(None, description="New lag in working days")
Expand Down
7 changes: 4 additions & 3 deletions src/tools/time_entries.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""Time entry management tools for time tracking."""

from typing import Optional
from pydantic import BaseModel, Field
from pydantic import Field
from src.server import mcp, get_client
from src.utils.inputs import CoercibleModel
from src.utils.formatting import format_success, format_error


class CreateTimeEntryInput(BaseModel):
class CreateTimeEntryInput(CoercibleModel):
"""Input model for creating time entries."""
work_package_id: int = Field(..., description="Work package ID", gt=0)
hours: float = Field(..., description="Hours spent", gt=0)
Expand All @@ -15,7 +16,7 @@ class CreateTimeEntryInput(BaseModel):
comment: Optional[str] = Field(None, description="Optional comment")


class UpdateTimeEntryInput(BaseModel):
class UpdateTimeEntryInput(CoercibleModel):
"""Input model for updating time entries."""
time_entry_id: int = Field(..., description="Time entry ID to update", gt=0)
hours: Optional[float] = Field(None, description="New hours spent", gt=0)
Expand Down
5 changes: 3 additions & 2 deletions src/tools/versions.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""Version/milestone management tools."""

from typing import Optional
from pydantic import BaseModel, Field
from pydantic import Field
from src.server import mcp, get_client
from src.utils.inputs import CoercibleModel
from src.utils.formatting import format_success, format_error


class CreateVersionInput(BaseModel):
class CreateVersionInput(CoercibleModel):
"""Input model for creating versions."""
project_id: int = Field(..., description="Project ID", gt=0)
name: str = Field(..., description="Version name", min_length=1, max_length=255)
Expand Down
7 changes: 4 additions & 3 deletions src/tools/weekly_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
import json
from typing import Optional
from datetime import datetime, timedelta
from pydantic import BaseModel, Field
from pydantic import Field

from src.server import mcp, get_client
from src.utils.inputs import CoercibleModel
from src.utils.formatting import format_success, format_error
from src.utils.report_formatter import (
format_weekly_report_markdown,
format_report_data_json,
)


class GenerateWeeklyReportInput(BaseModel):
class GenerateWeeklyReportInput(CoercibleModel):
"""Input model for generating weekly reports."""
project_id: int = Field(..., description="Project ID to generate report for", gt=0)
from_date: str = Field(..., description="Report start date (YYYY-MM-DD)")
Expand All @@ -23,7 +24,7 @@ class GenerateWeeklyReportInput(BaseModel):
format: str = Field("markdown", description="Output format: 'markdown' or 'json'")


class GetReportDataInput(BaseModel):
class GetReportDataInput(CoercibleModel):
"""Input model for getting raw report data."""
project_id: int = Field(..., description="Project ID", gt=0)
from_date: str = Field(..., description="Start date (YYYY-MM-DD)")
Expand Down
Loading