CopilotTaskMaster 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- copilottaskmaster-0.1.1.dist-info/METADATA +136 -0
- copilottaskmaster-0.1.1.dist-info/RECORD +12 -0
- copilottaskmaster-0.1.1.dist-info/WHEEL +5 -0
- copilottaskmaster-0.1.1.dist-info/entry_points.txt +3 -0
- copilottaskmaster-0.1.1.dist-info/top_level.txt +1 -0
- taskmaster/__init__.py +39 -0
- taskmaster/_version.py +34 -0
- taskmaster/cli.py +333 -0
- taskmaster/mcp_server.py +496 -0
- taskmaster/search.py +241 -0
- taskmaster/task_manager.py +388 -0
- taskmaster/utils.py +12 -0
taskmaster/mcp_server.py
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Server - Model Context Protocol integration for LLM interaction
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any, Dict, List
|
|
8
|
+
from mcp.server import Server
|
|
9
|
+
from mcp.server.stdio import stdio_server
|
|
10
|
+
from mcp.types import Tool, TextContent
|
|
11
|
+
|
|
12
|
+
from .task_manager import TaskManager
|
|
13
|
+
from .search import TaskSearcher
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Initialize server
|
|
17
|
+
app = Server("taskmaster")
|
|
18
|
+
|
|
19
|
+
# Managers (initialized on demand)
|
|
20
|
+
_task_manager = None
|
|
21
|
+
_task_searcher = None
|
|
22
|
+
|
|
23
|
+
def get_managers():
|
|
24
|
+
"""Get or initialize the task manager and searcher"""
|
|
25
|
+
global _task_manager, _task_searcher
|
|
26
|
+
if _task_manager is None:
|
|
27
|
+
tasks_dir = os.environ.get('TASKMASTER_TASKS_DIR', './tasks')
|
|
28
|
+
_task_manager = TaskManager(tasks_dir)
|
|
29
|
+
_task_searcher = TaskSearcher(tasks_dir)
|
|
30
|
+
return _task_manager, _task_searcher
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Module-level aliases for tests and external usage
|
|
34
|
+
# Tests expect `task_manager` and `task_searcher` to be importable
|
|
35
|
+
task_manager, task_searcher = get_managers()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.list_tools()
|
|
39
|
+
async def list_tools() -> List[Tool]:
|
|
40
|
+
"""List available tools for the LLM"""
|
|
41
|
+
return [
|
|
42
|
+
Tool(
|
|
43
|
+
name="create_task",
|
|
44
|
+
description="Create a new task card in markdown format",
|
|
45
|
+
inputSchema={
|
|
46
|
+
"type": "object",
|
|
47
|
+
"properties": {
|
|
48
|
+
"path": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"description": "Relative path for the task (e.g., 'project1/task1.md')"
|
|
51
|
+
},
|
|
52
|
+
"project": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "Project name to scope the task (optional)"
|
|
55
|
+
},
|
|
56
|
+
"title": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"description": "Task title"
|
|
59
|
+
},
|
|
60
|
+
"content": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"description": "Task content in markdown"
|
|
63
|
+
},
|
|
64
|
+
"status": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "Task status (open, in-progress, done, etc.)",
|
|
67
|
+
"default": "open"
|
|
68
|
+
},
|
|
69
|
+
"priority": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"description": "Task priority (low, medium, high, critical)",
|
|
72
|
+
"default": "medium"
|
|
73
|
+
},
|
|
74
|
+
"tags": {
|
|
75
|
+
"type": "array",
|
|
76
|
+
"items": {"type": "string"},
|
|
77
|
+
"description": "List of tags"
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"required": ["path", "title"]
|
|
81
|
+
}
|
|
82
|
+
),
|
|
83
|
+
Tool(
|
|
84
|
+
name="read_task",
|
|
85
|
+
description="Read a task card by its path",
|
|
86
|
+
inputSchema={
|
|
87
|
+
"type": "object",
|
|
88
|
+
"properties": {
|
|
89
|
+
"path": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"description": "Relative path to the task"
|
|
92
|
+
},
|
|
93
|
+
"project": {
|
|
94
|
+
"type": "string",
|
|
95
|
+
"description": "Project name to scope the task (optional)"
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"required": ["path"]
|
|
99
|
+
}
|
|
100
|
+
),
|
|
101
|
+
Tool(
|
|
102
|
+
name="update_task",
|
|
103
|
+
description="Update an existing task card",
|
|
104
|
+
inputSchema={
|
|
105
|
+
"type": "object",
|
|
106
|
+
"properties": {
|
|
107
|
+
"path": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"description": "Relative path to the task"
|
|
110
|
+
},
|
|
111
|
+
"project": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"description": "Project name to scope the task (optional)"
|
|
114
|
+
},
|
|
115
|
+
"title": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"description": "New title (optional)"
|
|
118
|
+
},
|
|
119
|
+
"content": {
|
|
120
|
+
"type": "string",
|
|
121
|
+
"description": "New content (optional)"
|
|
122
|
+
},
|
|
123
|
+
"metadata": {
|
|
124
|
+
"type": "object",
|
|
125
|
+
"description": "Metadata to update (merged with existing)"
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"required": ["path"]
|
|
129
|
+
}
|
|
130
|
+
),
|
|
131
|
+
Tool(
|
|
132
|
+
name="delete_task",
|
|
133
|
+
description="Delete a task card",
|
|
134
|
+
inputSchema={
|
|
135
|
+
"type": "object",
|
|
136
|
+
"properties": {
|
|
137
|
+
"path": {
|
|
138
|
+
"type": "string",
|
|
139
|
+
"description": "Relative path to the task"
|
|
140
|
+
},
|
|
141
|
+
"project": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"description": "Project name to scope the task (optional)"
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
"required": ["path"]
|
|
147
|
+
}
|
|
148
|
+
),
|
|
149
|
+
Tool(
|
|
150
|
+
name="list_tasks",
|
|
151
|
+
description="List tasks in a directory (token-efficient summary)",
|
|
152
|
+
inputSchema={
|
|
153
|
+
"type": "object",
|
|
154
|
+
"properties": {
|
|
155
|
+
"subpath": {
|
|
156
|
+
"type": "string",
|
|
157
|
+
"description": "Subdirectory to list (empty for all)",
|
|
158
|
+
"default": ""
|
|
159
|
+
},
|
|
160
|
+
"recursive": {
|
|
161
|
+
"type": "boolean",
|
|
162
|
+
"description": "Include subdirectories",
|
|
163
|
+
"default": True
|
|
164
|
+
},
|
|
165
|
+
"project": {
|
|
166
|
+
"type": "string",
|
|
167
|
+
"description": "Project name to scope the listing (optional)"
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
"required": []
|
|
171
|
+
}
|
|
172
|
+
),
|
|
173
|
+
Tool(
|
|
174
|
+
name="search_tasks",
|
|
175
|
+
description="Search tasks with text and metadata filters (token-efficient)",
|
|
176
|
+
inputSchema={
|
|
177
|
+
"type": "object",
|
|
178
|
+
"properties": {
|
|
179
|
+
"query": {
|
|
180
|
+
"type": "string",
|
|
181
|
+
"description": "Text to search in title and content"
|
|
182
|
+
},
|
|
183
|
+
"status": {
|
|
184
|
+
"type": "string",
|
|
185
|
+
"description": "Filter by status"
|
|
186
|
+
},
|
|
187
|
+
"priority": {
|
|
188
|
+
"type": "string",
|
|
189
|
+
"description": "Filter by priority"
|
|
190
|
+
},
|
|
191
|
+
"tags": {
|
|
192
|
+
"type": "array",
|
|
193
|
+
"items": {"type": "string"},
|
|
194
|
+
"description": "Filter by tags"
|
|
195
|
+
},
|
|
196
|
+
"path_pattern": {
|
|
197
|
+
"type": "string",
|
|
198
|
+
"description": "Path pattern to search (e.g., 'project1/**')"
|
|
199
|
+
},
|
|
200
|
+
"max_results": {
|
|
201
|
+
"type": "integer",
|
|
202
|
+
"description": "Maximum results to return",
|
|
203
|
+
"default": 20
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
"required": []
|
|
207
|
+
}
|
|
208
|
+
),
|
|
209
|
+
Tool(
|
|
210
|
+
name="get_structure",
|
|
211
|
+
description="Get hierarchical folder structure (token-efficient overview)",
|
|
212
|
+
inputSchema={
|
|
213
|
+
"type": "object",
|
|
214
|
+
"properties": {
|
|
215
|
+
"subpath": {
|
|
216
|
+
"type": "string",
|
|
217
|
+
"description": "Subdirectory to show structure for",
|
|
218
|
+
"default": ""
|
|
219
|
+
},
|
|
220
|
+
"project": {
|
|
221
|
+
"type": "string",
|
|
222
|
+
"description": "Project name to scope the structure (optional)"
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
"required": []
|
|
226
|
+
}
|
|
227
|
+
),
|
|
228
|
+
Tool(
|
|
229
|
+
name="move_task",
|
|
230
|
+
description="Move or rename a task",
|
|
231
|
+
inputSchema={
|
|
232
|
+
"type": "object",
|
|
233
|
+
"properties": {
|
|
234
|
+
"old_path": {
|
|
235
|
+
"type": "string",
|
|
236
|
+
"description": "Current path"
|
|
237
|
+
},
|
|
238
|
+
"new_path": {
|
|
239
|
+
"type": "string",
|
|
240
|
+
"description": "New path"
|
|
241
|
+
},
|
|
242
|
+
"project": {
|
|
243
|
+
"type": "string",
|
|
244
|
+
"description": "Project name to scope the move (optional)"
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
"required": ["old_path", "new_path"]
|
|
248
|
+
}
|
|
249
|
+
),
|
|
250
|
+
Tool(
|
|
251
|
+
name="get_all_tags",
|
|
252
|
+
description="Get all unique tags across all tasks",
|
|
253
|
+
inputSchema={
|
|
254
|
+
"type": "object",
|
|
255
|
+
"properties": {},
|
|
256
|
+
"required": []
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@app.call_tool()
|
|
263
|
+
async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
|
|
264
|
+
"""Handle tool calls from the LLM"""
|
|
265
|
+
task_manager, task_searcher = get_managers()
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
if name == "create_task":
|
|
269
|
+
from .utils import project_resolution_error_msg
|
|
270
|
+
|
|
271
|
+
metadata = {}
|
|
272
|
+
if 'status' in arguments:
|
|
273
|
+
metadata['status'] = arguments['status']
|
|
274
|
+
if 'priority' in arguments:
|
|
275
|
+
metadata['priority'] = arguments['priority']
|
|
276
|
+
if 'tags' in arguments:
|
|
277
|
+
metadata['tags'] = arguments['tags']
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
result = task_manager.create_task(
|
|
281
|
+
path=arguments['path'],
|
|
282
|
+
title=arguments['title'],
|
|
283
|
+
content=arguments.get('content', ''),
|
|
284
|
+
metadata=metadata if metadata else None,
|
|
285
|
+
project=arguments.get('project')
|
|
286
|
+
)
|
|
287
|
+
except ValueError as e:
|
|
288
|
+
return [TextContent(type="text", text=project_resolution_error_msg(e))]
|
|
289
|
+
|
|
290
|
+
return [TextContent(
|
|
291
|
+
type="text",
|
|
292
|
+
text=f"✓ Created task '{result['title']}' at {result['path']}"
|
|
293
|
+
)]
|
|
294
|
+
|
|
295
|
+
elif name == "read_task":
|
|
296
|
+
from .utils import project_resolution_error_msg
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
task = task_manager.read_task(arguments['path'], project=arguments.get('project'))
|
|
300
|
+
except ValueError as e:
|
|
301
|
+
return [TextContent(type="text", text=project_resolution_error_msg(e))]
|
|
302
|
+
|
|
303
|
+
if not task:
|
|
304
|
+
return [TextContent(
|
|
305
|
+
type="text",
|
|
306
|
+
text=f"✗ Task not found: {arguments['path']}"
|
|
307
|
+
)]
|
|
308
|
+
|
|
309
|
+
# Format task info in a token-efficient way
|
|
310
|
+
output = f"**{task['title']}**\n\n"
|
|
311
|
+
output += f"Path: {task['path']}\n\n"
|
|
312
|
+
|
|
313
|
+
# Key metadata
|
|
314
|
+
for key in ['status', 'priority', 'tags', 'created', 'updated']:
|
|
315
|
+
if key in task['metadata']:
|
|
316
|
+
output += f"{key.title()}: {task['metadata'][key]}\n"
|
|
317
|
+
|
|
318
|
+
output += f"\n---\n\n{task['content']}"
|
|
319
|
+
|
|
320
|
+
return [TextContent(type="text", text=output)]
|
|
321
|
+
|
|
322
|
+
elif name == "update_task":
|
|
323
|
+
from .utils import project_resolution_error_msg
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
result = task_manager.update_task(
|
|
327
|
+
path=arguments['path'],
|
|
328
|
+
title=arguments.get('title'),
|
|
329
|
+
content=arguments.get('content'),
|
|
330
|
+
metadata=arguments.get('metadata'),
|
|
331
|
+
project=arguments.get('project')
|
|
332
|
+
)
|
|
333
|
+
except ValueError as e:
|
|
334
|
+
return [TextContent(type="text", text=project_resolution_error_msg(e))]
|
|
335
|
+
|
|
336
|
+
if not result:
|
|
337
|
+
return [TextContent(
|
|
338
|
+
type="text",
|
|
339
|
+
text=f"✗ Task not found: {arguments['path']}"
|
|
340
|
+
)]
|
|
341
|
+
|
|
342
|
+
return [TextContent(
|
|
343
|
+
type="text",
|
|
344
|
+
text=f"✓ Updated task: {result['path']}"
|
|
345
|
+
)]
|
|
346
|
+
|
|
347
|
+
elif name == "delete_task":
|
|
348
|
+
from .utils import project_resolution_error_msg
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
success = task_manager.delete_task(arguments['path'], project=arguments.get('project'))
|
|
352
|
+
except ValueError as e:
|
|
353
|
+
return [TextContent(type="text", text=project_resolution_error_msg(e))]
|
|
354
|
+
|
|
355
|
+
return [TextContent(
|
|
356
|
+
type="text",
|
|
357
|
+
text=f"✓ Deleted task: {arguments['path']}" if success
|
|
358
|
+
else f"✗ Task not found: {arguments['path']}"
|
|
359
|
+
)]
|
|
360
|
+
|
|
361
|
+
elif name == "list_tasks":
|
|
362
|
+
from .utils import project_resolution_error_msg
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
tasks = task_manager.list_tasks(
|
|
366
|
+
subpath=arguments.get('subpath', ''),
|
|
367
|
+
recursive=arguments.get('recursive', True),
|
|
368
|
+
include_content=False,
|
|
369
|
+
project=arguments.get('project')
|
|
370
|
+
)
|
|
371
|
+
except ValueError as e:
|
|
372
|
+
return [TextContent(type="text", text=project_resolution_error_msg(e))]
|
|
373
|
+
|
|
374
|
+
if not tasks:
|
|
375
|
+
return [TextContent(type="text", text="No tasks found.")]
|
|
376
|
+
|
|
377
|
+
# Token-efficient summary
|
|
378
|
+
output = f"Found {len(tasks)} tasks:\n\n"
|
|
379
|
+
for task in tasks:
|
|
380
|
+
status = task['metadata'].get('status', 'unknown')
|
|
381
|
+
priority = task['metadata'].get('priority', '-')
|
|
382
|
+
output += f"• [{status}] {task['title']}\n Path: {task['path']} | Priority: {priority}\n"
|
|
383
|
+
|
|
384
|
+
return [TextContent(type="text", text=output)]
|
|
385
|
+
|
|
386
|
+
elif name == "search_tasks":
|
|
387
|
+
metadata_filters = {}
|
|
388
|
+
if 'status' in arguments:
|
|
389
|
+
metadata_filters['status'] = arguments['status']
|
|
390
|
+
if 'priority' in arguments:
|
|
391
|
+
metadata_filters['priority'] = arguments['priority']
|
|
392
|
+
if 'tags' in arguments:
|
|
393
|
+
metadata_filters['tags'] = arguments['tags']
|
|
394
|
+
|
|
395
|
+
results = task_searcher.search(
|
|
396
|
+
query=arguments.get('query', ''),
|
|
397
|
+
metadata_filters=metadata_filters if metadata_filters else None,
|
|
398
|
+
path_pattern=arguments.get('path_pattern', ''),
|
|
399
|
+
max_results=arguments.get('max_results', 20),
|
|
400
|
+
include_content=False
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
if not results:
|
|
404
|
+
return [TextContent(type="text", text="No tasks found.")]
|
|
405
|
+
|
|
406
|
+
# Token-efficient results
|
|
407
|
+
output = f"Found {len(results)} matching tasks:\n\n"
|
|
408
|
+
for r in results:
|
|
409
|
+
output += f"• {r['title']} (relevance: {r['score']})\n"
|
|
410
|
+
output += f" Path: {r['path']}\n"
|
|
411
|
+
if 'snippet' in r:
|
|
412
|
+
output += f" Snippet: {r['snippet'][:100]}...\n"
|
|
413
|
+
output += "\n"
|
|
414
|
+
|
|
415
|
+
return [TextContent(type="text", text=output)]
|
|
416
|
+
|
|
417
|
+
elif name == "get_structure":
|
|
418
|
+
from .utils import project_resolution_error_msg
|
|
419
|
+
|
|
420
|
+
try:
|
|
421
|
+
structure = task_manager.get_structure(arguments.get('subpath', ''), project=arguments.get('project'))
|
|
422
|
+
except ValueError as e:
|
|
423
|
+
return [TextContent(type="text", text=project_resolution_error_msg(e))]
|
|
424
|
+
|
|
425
|
+
def format_tree(node, indent=0):
|
|
426
|
+
result = " " * indent + f"📁 {node['name']}\n" if node['type'] == 'directory' else ""
|
|
427
|
+
|
|
428
|
+
if node['type'] == 'directory' and 'children' in node:
|
|
429
|
+
for child in node['children']:
|
|
430
|
+
if child['type'] == 'directory':
|
|
431
|
+
result += format_tree(child, indent + 1)
|
|
432
|
+
else:
|
|
433
|
+
status = child.get('metadata', {}).get('status', '?')
|
|
434
|
+
result += " " * (indent + 1) + f"📄 {child['title']} [{status}]\n"
|
|
435
|
+
|
|
436
|
+
return result
|
|
437
|
+
|
|
438
|
+
output = format_tree(structure)
|
|
439
|
+
return [TextContent(type="text", text=output or "Empty structure")]
|
|
440
|
+
|
|
441
|
+
elif name == "move_task":
|
|
442
|
+
from .utils import project_resolution_error_msg
|
|
443
|
+
|
|
444
|
+
try:
|
|
445
|
+
success = task_manager.move_task(
|
|
446
|
+
arguments['old_path'],
|
|
447
|
+
arguments['new_path'],
|
|
448
|
+
project=arguments.get('project')
|
|
449
|
+
)
|
|
450
|
+
except ValueError as e:
|
|
451
|
+
return [TextContent(type="text", text=project_resolution_error_msg(e))]
|
|
452
|
+
|
|
453
|
+
return [TextContent(
|
|
454
|
+
type="text",
|
|
455
|
+
text=f"✓ Moved task" if success else "✗ Failed to move task"
|
|
456
|
+
)]
|
|
457
|
+
|
|
458
|
+
elif name == "get_all_tags":
|
|
459
|
+
tags = task_searcher.get_all_tags(project=arguments.get('project'))
|
|
460
|
+
|
|
461
|
+
if not tags:
|
|
462
|
+
return [TextContent(type="text", text="No tags found.")]
|
|
463
|
+
|
|
464
|
+
output = "Tags: " + ", ".join(sorted(tags))
|
|
465
|
+
return [TextContent(type="text", text=output)]
|
|
466
|
+
|
|
467
|
+
else:
|
|
468
|
+
return [TextContent(
|
|
469
|
+
type="text",
|
|
470
|
+
text=f"Unknown tool: {name}"
|
|
471
|
+
)]
|
|
472
|
+
|
|
473
|
+
except Exception as e:
|
|
474
|
+
return [TextContent(
|
|
475
|
+
type="text",
|
|
476
|
+
text=f"Error executing {name}: {str(e)}"
|
|
477
|
+
)]
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
async def run_server():
|
|
484
|
+
"""Run the MCP server"""
|
|
485
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
486
|
+
await app.run(read_stream, write_stream, app.create_initialization_options())
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def main():
|
|
490
|
+
"""Main entry point for MCP server"""
|
|
491
|
+
import asyncio
|
|
492
|
+
asyncio.run(run_server())
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
if __name__ == '__main__':
|
|
496
|
+
main()
|