1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
"""MCP server exposing pygittools task boards to AI clients."""
from __future__ import annotations
import json
import os
from mcp.server import MCPServer
from pygittools.tasks import Task, _parse_dt, create_task
from pygittools.tasks_query import (
BoardColumnView,
BoardView,
RepoLocation,
TaskView,
board_ref,
discover_repo_path,
get_board,
get_board_tasks_grouped,
get_task_view,
list_boards,
list_tasks,
open_repository,
resolve_repo_location,
)
from pygittools_mcp.exceptions import (
BoardNotFoundError,
InvalidTaskDueDateError,
InvalidTaskPriorityError,
InvalidTaskStatusError,
TaskNotFoundError,
)
from pygittools_mcp.models import CreateTaskToolResult
mcp = MCPServer(
"pygittools-tasks",
instructions=(
"Read task boards stored as Git annotated tags. "
"Boards live under refs/tags/boards/, tasks under refs/tags/tasks/, "
"and comments under refs/tags/comments/. "
"Set PYGITTOOLS_REPO to a repository path, or PYGITWEB_PROJECTROOT plus a project name."
),
)
def _repo_for_tool(project: str | None = None) -> RepoLocation:
return resolve_repo_location(project=project)
@mcp.tool(title="List boards")
def list_boards_tool(project: str | None = None) -> list[BoardView]:
"""List all task boards in the configured repository."""
location = _repo_for_tool(project)
repo = open_repository(location)
return list_boards(repo)
@mcp.tool(title="List tasks")
def list_tasks_tool(
board: str = "Tasks",
project: str | None = None,
include_comments: bool = True,
) -> list[TaskView]:
"""List tasks on a board, optionally including comment bodies."""
location = _repo_for_tool(project)
repo = open_repository(location)
try:
return list_tasks(repo, board, include_comments=include_comments)
except KeyError as exc:
raise BoardNotFoundError(board) from exc
@mcp.tool(title="Get task")
def get_task_tool(task_ref: str, project: str | None = None) -> TaskView:
"""Get one task by ref slug (task_123) or full ref (refs/tags/tasks/task_123)."""
location = _repo_for_tool(project)
repo = open_repository(location)
try:
return get_task_view(repo, task_ref, include_comments=True)
except KeyError as exc:
raise TaskNotFoundError(task_ref) from exc
@mcp.tool(title="Create task")
def create_task_tool(
title: str,
description: str,
board: str = "Tasks",
status: str = "TODO",
priority: str = "LOW",
assignee: str = "",
due_date: str | None = None,
tags: str | None = None,
points: int | None = None,
project: str | None = None,
) -> CreateTaskToolResult:
"""Create a new task on a board."""
location = _repo_for_tool(project)
repo = open_repository(location)
try:
b = get_board(repo, board_ref(board))
if b is None:
raise BoardNotFoundError(board)
except KeyError as e:
raise BoardNotFoundError(board) from e
try:
s = Task.Status(status)
except ValueError as e:
raise InvalidTaskStatusError(status) from e
try:
p = Task.Priority(priority)
except ValueError as e:
raise InvalidTaskPriorityError(priority) from e
try:
d = _parse_dt(due_date)
except ValueError as e:
raise InvalidTaskDueDateError(due_date or "") from e
try:
result = create_task(
repo=repo,
board=b,
title=title,
description=description,
status=s,
priority=p,
assignee=assignee,
due_date=d,
tags=tags or "",
points=points,
)
return {"task_id": result["id"], "ref": result["ref"], "oid": str(result["oid"])}
except Exception:
raise
@mcp.tool(title="Board grouped by status")
def board_by_status_tool(
board: str = "Tasks",
project: str | None = None,
include_comments: bool = True,
) -> list[BoardColumnView]:
"""Return board tasks grouped into status columns (TODO, IN_PROGRESS, etc.)."""
location = _repo_for_tool(project)
repo = open_repository(location)
try:
return get_board_tasks_grouped(repo, board, include_comments=include_comments)
except KeyError as exc:
raise ValueError(str(exc)) from exc
@mcp.resource("board://{board_name}")
def board_resource(board_name: str) -> str:
"""JSON snapshot of a board grouped by status."""
location = resolve_repo_location()
repo = open_repository(location)
columns = get_board_tasks_grouped(repo, board_name, include_comments=True)
return json.dumps(columns, indent=2)
@mcp.resource("task://{task_ref}")
def task_resource(task_ref: str) -> str:
"""JSON snapshot of a single task with comments."""
location = resolve_repo_location()
repo = open_repository(location)
task = get_task_view(repo, task_ref, include_comments=True)
return json.dumps(task, indent=2)
def main() -> None:
repo_env = os.environ.get("PYGITTOOLS_REPO", "").strip()
if not repo_env:
try:
discovered = discover_repo_path()
except FileNotFoundError:
discovered = None
if discovered is not None:
os.environ["PYGITTOOLS_REPO"] = str(discovered)
mcp.run(transport="stdio")
if __name__ == "__main__":
main()