Skip to content

Commit 65137ef

Browse files
committed
feat: stop tracking task & get active tracking task
* stop tracking task * get active tracking task * some enhancements
1 parent 8adc5cb commit 65137ef

4 files changed

Lines changed: 119 additions & 104 deletions

File tree

.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
LINEAR_API_KEY=your_linear_api_key_here
2-
TIMETRACKING_API_KEY=your_trackingtime_api_key_here
32
# can be empty
4-
LINEAR_TEAM=your_team
3+
LINEAR_TEAM=your_linear_team
4+
TRACKINGTIME_API_KEY=your_trackingtime_api_key_here
Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,77 @@
1-
from typing import Optional, Union
1+
from typing import Optional
22
import aiohttp
33
from datetime import datetime, timezone
44
from task_tracker.config import settings
55

66

7-
class TimeTrackingClient:
7+
class TrackingTimeClient:
88
def __init__(self):
9-
self.api_key = settings.TIMETRACKING_API_KEY
9+
self.api_key = settings.TRACKINGTIME_API_KEY
1010
self.base_url = "https://app.trackingtime.co/api/v4"
1111
self.auth = aiohttp.BasicAuth.decode(f"Basic {self.api_key}")
1212
self.headers = {"Content-Type": "application/json"}
1313

14-
async def start_tracking(self, project: str, description: str) -> Union[dict, str]:
15-
"""Start tracking a task
14+
async def start_tracking(self, project: str, description: str) -> dict:
15+
"""Start time tracking a task
1616
1717
Args:
18-
project: Name of the project to create the task in
19-
description: Description/name of the task
18+
project: name of the project to create the task in
19+
description: description of the task
2020
2121
Returns:
22-
Union[dict, str]: Response from TrackingTime API - either JSON dict or raw text if not JSON
22+
dict: json containing the tracking_task_id
2323
"""
24-
async with aiohttp.ClientSession(auth=self.auth) as session:
24+
async with aiohttp.ClientSession(auth=self.auth, headers=self.headers) as session:
2525
current_time = datetime.now(
2626
timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
2727
async with session.post(
2828
f"{self.base_url}/tasks/track",
2929
params={
3030
"date": current_time,
3131
"task_name": description,
32-
"project_name": project
32+
"project_name": project,
33+
"return_task": "true"
3334
}
3435
) as response:
3536
response.raise_for_status()
36-
return await response.json()
37+
full_response = await response.json()
38+
return {"tracking_task_id": full_response["data"]["id"]}
3739

38-
async def stop_tracking(self, entry_id: str) -> dict:
39-
"""Stop time tracking for a task using TrackingTime API
40+
async def stop_tracking(self, task_id: str) -> dict:
41+
"""Stop time tracking a task given its ID
4042
4143
Args:
42-
entry_id: The ID of the time entry to stop
44+
task_id: The ID of the task to stop
4345
4446
Returns:
45-
dict: Response from TrackingTime API containing the completed time entry
47+
dict: Response from TrackingTime API containing the task details
4648
"""
4749
async with aiohttp.ClientSession(auth=self.auth, headers=self.headers) as session:
50+
current_time = datetime.now(
51+
timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
4852
async with session.post(
49-
f"{self.base_url}/time_entries/{entry_id}/stop",
50-
json={
51-
"end_date": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
53+
f"{self.base_url}/tasks/stop",
54+
params={
55+
"date": current_time,
56+
"task_id": task_id,
57+
"return_task": "true"
5258
}
5359
) as response:
5460
response.raise_for_status()
5561
return await response.json()
5662

57-
async def get_active_tracking(self) -> Optional[dict]:
58-
"""Get currently active tracking session if any using TrackingTime API
63+
async def get_tracking_task(self, filter: str = "TRACKING") -> Optional[dict]:
64+
"""Fetch the tracking task with the given filter, defaults to "TRACKING"
5965
6066
Returns:
6167
Optional[dict]: Active time entry if exists, None otherwise
6268
"""
6369
async with aiohttp.ClientSession(auth=self.auth, headers=self.headers) as session:
64-
async with session.get(f"{self.base_url}/time_entries/current") as response:
70+
async with session.get(
71+
f"{self.base_url}/tasks",
72+
params={
73+
"filter": filter
74+
}
75+
) as response:
6576
response.raise_for_status()
6677
return await response.json()

src/task_tracker/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ class Settings(BaseSettings):
44
LINEAR_API_KEY: str
55
LINEAR_TEAM: str | None = None
66

7-
TIMETRACKING_API_KEY: str
7+
TRACKINGTIME_API_KEY: str
88

99
class Config:
1010
env_file = ".env"

src/task_tracker/server.py

Lines changed: 84 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,24 @@
33
import logging
44
from typing import Any, Dict, List
55

6-
from mcp.server.models import InitializationOptions
76
from mcp.server import NotificationOptions, Server
7+
from mcp.server.models import InitializationOptions
88
from mcp.server.stdio import stdio_server
99
import mcp.types as types
1010

1111
from task_tracker.api.linear_client import LinearClient
12-
from task_tracker.api.timetracking_client import TimeTrackingClient
13-
14-
linear_client = asyncio.run(LinearClient.create())
15-
timetracking_client = TimeTrackingClient()
16-
server = Server("task-tracker")
12+
from task_tracker.api.trackingtime_client import TrackingTimeClient
1713

1814
logging.basicConfig(
1915
level=logging.INFO,
2016
format='%(asctime)s - %(levelname)s - %(message)s'
2117
)
2218
logger = logging.getLogger("task-tracker")
23-
logger.info("task-tracker server started")
19+
logger.info("starting task-tracker server")
20+
21+
linear_client = asyncio.run(LinearClient.create())
22+
trackingtime_client = TrackingTimeClient()
23+
server = Server("task-tracker")
2424

2525

2626
@server.list_tools()
@@ -42,88 +42,91 @@ async def handle_list_tools() -> List[types.Tool]:
4242
}
4343
),
4444
types.Tool(
45-
name="log_time",
46-
description="Log time for a task",
45+
name="set_current_team",
46+
description="Set the current Linear team by name",
4747
inputSchema={
4848
"type": "object",
4949
"properties": {
50-
"task_id": {"type": "string"},
51-
"duration": {"type": "integer"},
52-
"description": {"type": "string"}
50+
"team_name": {"type": "string"}
5351
},
54-
"required": ["task_id", "duration", "description"]
52+
"required": ["team_name"]
5553
}
5654
),
5755
types.Tool(
58-
name="start_tracking",
59-
description="Start time tracking for a task",
56+
name="get_my_tasks",
57+
description=(
58+
"Get the Linear tasks assigned to me. "
59+
"Support task status: backlog, unstarted, started, completed, canceled, triage. "
60+
"Default is unstarted."
61+
),
6062
inputSchema={
6163
"type": "object",
6264
"properties": {
63-
"project": {
64-
"type": "string",
65-
"description": "task project"
66-
},
67-
"description": {
68-
"type": "string",
69-
"description": "task name"
65+
"status": {
66+
"type": "array",
67+
"items": {"type": "string"},
68+
"optional": True,
69+
"description": "List of task status to filter by"
7070
}
7171
},
72-
"required": ["project", "description"]
72+
"required": []
7373
}
7474
),
7575
types.Tool(
76-
name="stop_tracking",
77-
description="Stop current time tracking",
76+
name="search_tasks",
77+
description="Search Linear tasks by title or identifier",
7878
inputSchema={
7979
"type": "object",
8080
"properties": {
81-
"entry_id": {"type": "string"}
81+
"search_term": {
82+
"type": "string",
83+
"description": "Text to search for in task titles or identifiers"
84+
}
8285
},
83-
"required": ["entry_id"]
86+
"required": ["search_term"]
8487
}
8588
),
8689
types.Tool(
87-
name="set_current_team",
88-
description="Set the current team by name",
90+
name="start_tracking",
91+
description="Start time tracking for a task",
8992
inputSchema={
9093
"type": "object",
9194
"properties": {
92-
"team_name": {"type": "string"}
95+
"project": {
96+
"type": "string",
97+
"description": "task project"
98+
},
99+
"description": {
100+
"type": "string",
101+
"description": "task name"
102+
}
93103
},
94-
"required": ["team_name"]
104+
"required": ["project", "description"]
95105
}
96106
),
97107
types.Tool(
98-
name="get_my_tasks",
99-
description="Get tasks assigned to me. Support task status: backlog, unstarted, started, completed, canceled, triage. Default is unstarted.",
108+
name="stop_tracking",
109+
description="Stop current time tracking",
100110
inputSchema={
101111
"type": "object",
102112
"properties": {
103-
"status": {
104-
"type": "array",
105-
"items": {"type": "string"},
106-
"optional": True,
107-
"description": "List of task status to filter by"
113+
"task_id": {
114+
"type": "string",
115+
"description": "task id"
108116
}
109117
},
110-
"required": []
118+
"required": ["task_id"]
111119
}
112120
),
113121
types.Tool(
114-
name="search_tasks",
115-
description="Search for tasks by title or identifier",
122+
name="get_active_tracking",
123+
description="Get the currently active tracking task",
116124
inputSchema={
117125
"type": "object",
118-
"properties": {
119-
"search_term": {
120-
"type": "string",
121-
"description": "Text to search for in task titles or identifiers"
122-
}
123-
},
124-
"required": ["search_term"]
126+
"properties": {},
127+
"required": []
125128
}
126-
)
129+
),
127130
]
128131

129132

@@ -147,37 +150,6 @@ async def handle_call_tool(name: str, arguments: Dict[str, Any] | None) -> List[
147150
json.dumps(result, indent=2)}"
148151
)]
149152

150-
elif name == "log_time":
151-
result = await timetracking_client.log_time(
152-
task_id=arguments["task_id"],
153-
duration=arguments["duration"],
154-
description=arguments["description"]
155-
)
156-
return [types.TextContent(
157-
type="text",
158-
text=f"Time logged successfully: {
159-
json.dumps(result, indent=2)}"
160-
)]
161-
162-
elif name == "start_tracking":
163-
result = await timetracking_client.start_tracking(
164-
project=arguments["project"],
165-
description=arguments["description"]
166-
)
167-
return [types.TextContent(
168-
type="text",
169-
text=f"Time tracking started: {json.dumps(result, indent=2)}"
170-
)]
171-
172-
elif name == "stop_tracking":
173-
result = await timetracking_client.stop_tracking(
174-
entry_id=arguments["entry_id"]
175-
)
176-
return [types.TextContent(
177-
type="text",
178-
text=f"Time tracking stopped: {json.dumps(result, indent=2)}"
179-
)]
180-
181153
elif name == "set_current_team":
182154
team_name = arguments["team_name"]
183155
await linear_client.set_current_team(team_name)
@@ -215,6 +187,38 @@ async def handle_call_tool(name: str, arguments: Dict[str, Any] | None) -> List[
215187
json.dumps(tasks, indent=2)}"
216188
)]
217189

190+
elif name == "start_tracking":
191+
result = await trackingtime_client.start_tracking(
192+
project=arguments["project"],
193+
description=arguments["description"]
194+
)
195+
return [types.TextContent(
196+
type="text",
197+
text=f"Time tracking started: {json.dumps(result, indent=2)}"
198+
)]
199+
200+
elif name == "stop_tracking":
201+
result = await trackingtime_client.stop_tracking(
202+
task_id=arguments["task_id"]
203+
)
204+
return [types.TextContent(
205+
type="text",
206+
text=f"Time tracking stopped: {json.dumps(result, indent=2)}"
207+
)]
208+
209+
elif name == "get_active_tracking":
210+
result = await trackingtime_client.get_tracking_task(filter="TRACKING")
211+
if not result:
212+
return [types.TextContent(
213+
type="text",
214+
text="No active time tracking task found"
215+
)]
216+
return [types.TextContent(
217+
type="text",
218+
text=f"Current tracking task:\n{
219+
json.dumps(result, indent=2)}"
220+
)]
221+
218222
raise ValueError(f"Unknown tool: {name}")
219223

220224
except Exception as e:

0 commit comments

Comments
 (0)