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.
@@ -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()