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
"""MCP server exposing pygittools task boards to AI clients."""
from __future__ import annotations
import json
import os
from mcp.server.fastmcp import FastMCP
from pygittools.tasks_query import (
BoardColumnView,
BoardView,
RepoLocation,
TaskView,
discover_repo_path,
get_board_tasks_grouped,
get_task_view,
list_boards,
list_tasks,
open_repository,
resolve_repo_location,
)
mcp = FastMCP(
"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."
),
json_response=True,
)
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 ValueError(str(exc)) 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 ValueError(str(exc)) from exc
@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()