project-mcp 3.1.0 → 3.2.0
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.
- package/README.md +81 -48
- package/package.json +1 -1
- package/src/tools/backlog.js +177 -1
- package/src/tools/project-files.js +128 -1
- package/src/tools/tasks.js +118 -1
package/README.md
CHANGED
|
@@ -14,18 +14,52 @@ not just directory names.
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
+
# ⚡ Quick Start
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install project-mcp
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Configure
|
|
26
|
+
|
|
27
|
+
Add to `.mcp.json`:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"mcpServers": {
|
|
32
|
+
"project": {
|
|
33
|
+
"command": "npx",
|
|
34
|
+
"args": ["-y", "project-mcp"]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**That's it.** The server automatically finds and indexes:
|
|
41
|
+
|
|
42
|
+
- `.project/` — Operational truth (plans, todos, status)
|
|
43
|
+
- Root markdown files — README.md, DEVELOPMENT.md, etc.
|
|
44
|
+
- `docs/` — Reference documentation
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
17
48
|
## Table of Contents
|
|
18
49
|
|
|
19
50
|
- [project-mcp](#project-mcp)
|
|
51
|
+
- [⚡ Quick Start](#-quick-start)
|
|
52
|
+
- [Install](#install)
|
|
53
|
+
- [Configure](#configure)
|
|
20
54
|
- [Table of Contents](#table-of-contents)
|
|
21
|
-
- [⚡ Quick Start](#-quick-start)
|
|
22
|
-
- [Install](#install)
|
|
23
|
-
- [Configure](#configure)
|
|
24
55
|
- [🎯 Why project-mcp?](#-why-project-mcp)
|
|
25
|
-
- [🛠️ Available Tools](#️-available-tools)
|
|
56
|
+
- [🛠️ Available Tools (37)](#️-available-tools-37)
|
|
26
57
|
- [Search Tools](#search-tools)
|
|
27
58
|
- [Project Management Tools](#project-management-tools)
|
|
59
|
+
- [Backlog Tools](#backlog-tools)
|
|
28
60
|
- [Task Management Tools](#task-management-tools)
|
|
61
|
+
- [Archive Tools](#archive-tools)
|
|
62
|
+
- [Decision \& Status Tools](#decision--status-tools)
|
|
29
63
|
- [Quality Tools](#quality-tools)
|
|
30
64
|
- [📋 Task Management System](#-task-management-system)
|
|
31
65
|
- [Workflow](#workflow)
|
|
@@ -57,37 +91,6 @@ not just directory names.
|
|
|
57
91
|
|
|
58
92
|
---
|
|
59
93
|
|
|
60
|
-
## ⚡ Quick Start
|
|
61
|
-
|
|
62
|
-
### Install
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
npm install project-mcp
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### Configure
|
|
69
|
-
|
|
70
|
-
Add to `.mcp.json`:
|
|
71
|
-
|
|
72
|
-
```json
|
|
73
|
-
{
|
|
74
|
-
"mcpServers": {
|
|
75
|
-
"project": {
|
|
76
|
-
"command": "npx",
|
|
77
|
-
"args": ["-y", "project-mcp"]
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
**That's it.** The server automatically finds and indexes:
|
|
84
|
-
|
|
85
|
-
- `.project/` — Operational truth (plans, todos, status)
|
|
86
|
-
- Root markdown files — README.md, DEVELOPMENT.md, etc.
|
|
87
|
-
- `docs/` — Reference documentation
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
94
|
## 🎯 Why project-mcp?
|
|
92
95
|
|
|
93
96
|
**The Problem:** AI agents need to search project documentation, but:
|
|
@@ -108,7 +111,7 @@ automatically:
|
|
|
108
111
|
|
|
109
112
|
---
|
|
110
113
|
|
|
111
|
-
## 🛠️ Available Tools
|
|
114
|
+
## 🛠️ Available Tools (37)
|
|
112
115
|
|
|
113
116
|
### Search Tools
|
|
114
117
|
|
|
@@ -133,18 +136,48 @@ automatically:
|
|
|
133
136
|
| `create_or_update_decisions` | Create or update DECISIONS.md | Recording architecture decisions |
|
|
134
137
|
| `check_project_state` | Check which project files exist | Before making changes |
|
|
135
138
|
|
|
139
|
+
### Backlog Tools
|
|
140
|
+
|
|
141
|
+
| Tool | Description | Use When |
|
|
142
|
+
| --------------------- | ---------------------------------------------------- | ------------------------------- |
|
|
143
|
+
| `add_to_backlog` | Add single item to BACKLOG.md | Quick task creation |
|
|
144
|
+
| `get_backlog` | Read backlog with filtering/sorting | Viewing queued work |
|
|
145
|
+
| `update_backlog_item` | Update priority, title, tags, phase | Adjusting backlog items |
|
|
146
|
+
| `remove_from_backlog` | Delete item without promoting | Removing cancelled work |
|
|
147
|
+
| `import_tasks` | Parse plan/roadmap and bulk add to BACKLOG.md | Populating from roadmap |
|
|
148
|
+
| `promote_task` | Move task from BACKLOG to active work (creates YAML) | Starting work on a backlog item |
|
|
149
|
+
|
|
136
150
|
### Task Management Tools
|
|
137
151
|
|
|
138
|
-
| Tool | Description
|
|
139
|
-
| ----------------- |
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
143
|
-
| `
|
|
144
|
-
| `
|
|
145
|
-
| `
|
|
146
|
-
| `
|
|
147
|
-
| `sync_todo_index` | Generate TODO.md dashboard from active tasks
|
|
152
|
+
| Tool | Description | Use When |
|
|
153
|
+
| ----------------- | --------------------------------------------- | ------------------------ |
|
|
154
|
+
| `create_task` | Create active task directly (bypass backlog) | Urgent/immediate work |
|
|
155
|
+
| `get_task` | Read specific task by ID with full details | Viewing task details |
|
|
156
|
+
| `update_task` | Update any task field, transition status | Modifying existing tasks |
|
|
157
|
+
| `delete_task` | Permanently remove a task (with confirmation) | Removing cancelled tasks |
|
|
158
|
+
| `search_tasks` | Search tasks by keyword in title/content | Finding specific tasks |
|
|
159
|
+
| `get_next_task` | Get dependency-aware next task(s) to work on | Determining what to do |
|
|
160
|
+
| `list_tasks` | List/filter tasks with summary dashboard | Reviewing all tasks |
|
|
161
|
+
| `sync_todo_index` | Generate TODO.md dashboard from active tasks | Updating the overview |
|
|
162
|
+
|
|
163
|
+
### Archive Tools
|
|
164
|
+
|
|
165
|
+
| Tool | Description | Use When |
|
|
166
|
+
| --------------------- | ------------------------------------ | --------------------------- |
|
|
167
|
+
| `archive_task` | Move completed task to archive/ | Cleaning up done work |
|
|
168
|
+
| `list_archived_tasks` | List tasks in archive with filtering | Reviewing completed history |
|
|
169
|
+
| `unarchive_task` | Restore task from archive to active | Reopening completed work |
|
|
170
|
+
|
|
171
|
+
### Decision & Status Tools
|
|
172
|
+
|
|
173
|
+
| Tool | Description | Use When |
|
|
174
|
+
| ----------------------- | --------------------------------- | -------------------------------- |
|
|
175
|
+
| `add_decision` | Record ADR with structured format | Documenting architecture choices |
|
|
176
|
+
| `get_decision` | Read specific decision by ADR ID | Viewing decision details |
|
|
177
|
+
| `list_decisions` | List/filter architecture ADRs | Reviewing past decisions |
|
|
178
|
+
| `update_project_status` | Quick timestamped status update | Reporting progress |
|
|
179
|
+
| `add_roadmap_milestone` | Add milestone with deliverables | Planning future work |
|
|
180
|
+
| `get_roadmap` | Read roadmap content | Viewing planned work |
|
|
148
181
|
|
|
149
182
|
### Quality Tools
|
|
150
183
|
|
|
@@ -505,7 +538,7 @@ npm install
|
|
|
505
538
|
npm test
|
|
506
539
|
|
|
507
540
|
# Test the server
|
|
508
|
-
node index.js
|
|
541
|
+
node src/index.js
|
|
509
542
|
```
|
|
510
543
|
|
|
511
544
|
---
|
|
@@ -516,7 +549,7 @@ node index.js
|
|
|
516
549
|
- **[Contributing](CONTRIBUTING.md)** — How to contribute
|
|
517
550
|
- **[Security](SECURITY.md)** — Security policy
|
|
518
551
|
- **[Changelog](CHANGELOG.md)** — Version history
|
|
519
|
-
- **[Release Notes
|
|
552
|
+
- **[Release Notes v2.0.0](docs/releases/RELEASE_NOTES_v2.0.0.md)** — Latest
|
|
520
553
|
release
|
|
521
554
|
|
|
522
555
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Intent-based MCP server for project documentation search. Maps natural language queries to the right sources automatically—no configuration needed. The standard for AI agent documentation search.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/tools/backlog.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Backlog management tools.
|
|
3
|
-
* Handles: import_tasks, promote_task, archive_task, add_to_backlog, get_backlog, update_backlog_item, remove_from_backlog
|
|
3
|
+
* Handles: import_tasks, promote_task, archive_task, add_to_backlog, get_backlog, update_backlog_item, remove_from_backlog, list_archived_tasks, unarchive_task
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import {
|
|
@@ -245,6 +245,46 @@ export const definitions = [
|
|
|
245
245
|
required: ['task_id'],
|
|
246
246
|
},
|
|
247
247
|
},
|
|
248
|
+
{
|
|
249
|
+
name: 'list_archived_tasks',
|
|
250
|
+
description:
|
|
251
|
+
'Lists tasks in the archive/ directory. Shows completed work history with optional filtering by project or date.',
|
|
252
|
+
inputSchema: {
|
|
253
|
+
type: 'object',
|
|
254
|
+
properties: {
|
|
255
|
+
project: {
|
|
256
|
+
type: 'string',
|
|
257
|
+
description: 'Filter by project prefix.',
|
|
258
|
+
},
|
|
259
|
+
limit: {
|
|
260
|
+
type: 'number',
|
|
261
|
+
description: 'Maximum number of tasks to return. Default: 20.',
|
|
262
|
+
default: 20,
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: 'unarchive_task',
|
|
269
|
+
description:
|
|
270
|
+
'Restores a task from archive/ back to todos/ for further work. Use when a completed task needs to be reopened.',
|
|
271
|
+
inputSchema: {
|
|
272
|
+
type: 'object',
|
|
273
|
+
properties: {
|
|
274
|
+
task_id: {
|
|
275
|
+
type: 'string',
|
|
276
|
+
description: 'The task ID to unarchive (e.g., "AUTH-001").',
|
|
277
|
+
},
|
|
278
|
+
status: {
|
|
279
|
+
type: 'string',
|
|
280
|
+
description: 'Status to set on restore. Default: "todo".',
|
|
281
|
+
enum: ['todo', 'in_progress', 'blocked', 'review'],
|
|
282
|
+
default: 'todo',
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
required: ['task_id'],
|
|
286
|
+
},
|
|
287
|
+
},
|
|
248
288
|
];
|
|
249
289
|
|
|
250
290
|
/**
|
|
@@ -937,6 +977,140 @@ async function removeFromBacklog(args) {
|
|
|
937
977
|
};
|
|
938
978
|
}
|
|
939
979
|
|
|
980
|
+
/**
|
|
981
|
+
* List archived tasks handler
|
|
982
|
+
*/
|
|
983
|
+
async function listArchivedTasks(args) {
|
|
984
|
+
const { project, limit = 20 } = args || {};
|
|
985
|
+
|
|
986
|
+
await ensureArchiveDir();
|
|
987
|
+
|
|
988
|
+
const { readdir } = await import('fs/promises');
|
|
989
|
+
let files;
|
|
990
|
+
try {
|
|
991
|
+
files = await readdir(ARCHIVE_DIR);
|
|
992
|
+
} catch {
|
|
993
|
+
return {
|
|
994
|
+
content: [
|
|
995
|
+
{
|
|
996
|
+
type: 'text',
|
|
997
|
+
text: `⚠️ No archive directory found. Completed tasks will appear here after using \`archive_task\`.`,
|
|
998
|
+
},
|
|
999
|
+
],
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const mdFiles = files.filter((f) => f.endsWith('.md'));
|
|
1004
|
+
if (mdFiles.length === 0) {
|
|
1005
|
+
return {
|
|
1006
|
+
content: [
|
|
1007
|
+
{ type: 'text', text: `📦 Archive is empty. No completed tasks have been archived yet.` },
|
|
1008
|
+
],
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Load archived tasks
|
|
1013
|
+
const tasks = [];
|
|
1014
|
+
for (const file of mdFiles) {
|
|
1015
|
+
const filePath = join(ARCHIVE_DIR, file);
|
|
1016
|
+
const content = await readFile(filePath, 'utf-8');
|
|
1017
|
+
const parsed = matter(content);
|
|
1018
|
+
tasks.push({
|
|
1019
|
+
...parsed.data,
|
|
1020
|
+
file,
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Filter by project if specified
|
|
1025
|
+
let filtered = tasks;
|
|
1026
|
+
if (project) {
|
|
1027
|
+
filtered = filtered.filter((t) => t.project === project.toUpperCase());
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Sort by archived date (newest first)
|
|
1031
|
+
filtered.sort((a, b) => {
|
|
1032
|
+
const aDate = a.archived || a.completed || a.updated || '';
|
|
1033
|
+
const bDate = b.archived || b.completed || b.updated || '';
|
|
1034
|
+
return bDate.localeCompare(aDate);
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
const results = filtered.slice(0, limit);
|
|
1038
|
+
|
|
1039
|
+
let result = `## Archived Tasks\n\n`;
|
|
1040
|
+
result += `**Total:** ${filtered.length} task(s)${filtered.length > limit ? ` (showing ${limit})` : ''}\n\n`;
|
|
1041
|
+
|
|
1042
|
+
if (results.length === 0) {
|
|
1043
|
+
result += `*No archived tasks${project ? ` for project ${project.toUpperCase()}` : ''}.*\n`;
|
|
1044
|
+
} else {
|
|
1045
|
+
result += `| ID | Title | Completed | Archived |\n`;
|
|
1046
|
+
result += `|----|-------|-----------|----------|\n`;
|
|
1047
|
+
for (const task of results) {
|
|
1048
|
+
result += `| ${task.id} | ${task.title?.substring(0, 35)}${task.title?.length > 35 ? '...' : ''} | ${task.completed || '-'} | ${task.archived || '-'} |\n`;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
result += `\n---\n**Tools:** \`unarchive_task\` to restore | \`search_tasks\` with \`include_archived: true\` to search`;
|
|
1053
|
+
|
|
1054
|
+
return {
|
|
1055
|
+
content: [{ type: 'text', text: result }],
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Unarchive task handler
|
|
1061
|
+
*/
|
|
1062
|
+
async function unarchiveTask(args) {
|
|
1063
|
+
const { task_id, status = 'todo' } = args;
|
|
1064
|
+
|
|
1065
|
+
await ensureTodosDir();
|
|
1066
|
+
await ensureArchiveDir();
|
|
1067
|
+
|
|
1068
|
+
const id = task_id.toUpperCase();
|
|
1069
|
+
const archiveFile = join(ARCHIVE_DIR, `${id}.md`);
|
|
1070
|
+
const activeFile = join(TODOS_DIR, `${id}.md`);
|
|
1071
|
+
|
|
1072
|
+
if (!(await fileExists(archiveFile))) {
|
|
1073
|
+
return {
|
|
1074
|
+
content: [{ type: 'text', text: `❌ Task ${id} not found in archive/` }],
|
|
1075
|
+
isError: true,
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (await fileExists(activeFile)) {
|
|
1080
|
+
return {
|
|
1081
|
+
content: [{ type: 'text', text: `⚠️ Task ${id} already exists in todos/. Cannot unarchive.` }],
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Read archived task
|
|
1086
|
+
const content = await readFile(archiveFile, 'utf-8');
|
|
1087
|
+
const parsed = matter(content);
|
|
1088
|
+
|
|
1089
|
+
// Update metadata
|
|
1090
|
+
parsed.data.status = status;
|
|
1091
|
+
parsed.data.updated = getISODate();
|
|
1092
|
+
delete parsed.data.archived;
|
|
1093
|
+
if (status !== 'done') {
|
|
1094
|
+
delete parsed.data.completed;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const updatedContent = matter.stringify(parsed.content, parsed.data);
|
|
1098
|
+
|
|
1099
|
+
// Move from archive to todos
|
|
1100
|
+
await writeFile(activeFile, updatedContent, 'utf-8');
|
|
1101
|
+
await unlink(archiveFile);
|
|
1102
|
+
|
|
1103
|
+
let result = `## Task Unarchived: ${id}\n\n`;
|
|
1104
|
+
result += `**Title:** ${parsed.data.title}\n`;
|
|
1105
|
+
result += `**Status:** ${status}\n`;
|
|
1106
|
+
result += `**File:** \`todos/${id}.md\`\n\n`;
|
|
1107
|
+
result += `✅ Task restored from archive. It will now appear in \`get_next_task\` and \`list_tasks\`.`;
|
|
1108
|
+
|
|
1109
|
+
return {
|
|
1110
|
+
content: [{ type: 'text', text: result }],
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
|
|
940
1114
|
/**
|
|
941
1115
|
* Handler map
|
|
942
1116
|
*/
|
|
@@ -948,4 +1122,6 @@ export const handlers = {
|
|
|
948
1122
|
get_backlog: getBacklog,
|
|
949
1123
|
update_backlog_item: updateBacklogItem,
|
|
950
1124
|
remove_from_backlog: removeFromBacklog,
|
|
1125
|
+
list_archived_tasks: listArchivedTasks,
|
|
1126
|
+
unarchive_task: unarchiveTask,
|
|
951
1127
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project file management tools.
|
|
3
3
|
* Handles: manage_project_file, check_project_state, create_or_update_* tools,
|
|
4
|
-
* add_decision, list_decisions, update_project_status, add_roadmap_milestone
|
|
4
|
+
* add_decision, list_decisions, get_decision, update_project_status, add_roadmap_milestone, get_roadmap
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { PROJECT_DIR, TODO_SECTIONS } from '../lib/constants.js';
|
|
@@ -311,6 +311,35 @@ export const definitions = [
|
|
|
311
311
|
required: ['title'],
|
|
312
312
|
},
|
|
313
313
|
},
|
|
314
|
+
{
|
|
315
|
+
name: 'get_decision',
|
|
316
|
+
description:
|
|
317
|
+
'Reads a specific architecture decision by ADR ID. Returns the full decision content including context, decision, and consequences.',
|
|
318
|
+
inputSchema: {
|
|
319
|
+
type: 'object',
|
|
320
|
+
properties: {
|
|
321
|
+
id: {
|
|
322
|
+
type: 'string',
|
|
323
|
+
description: 'The ADR ID to retrieve (e.g., "ADR-001", "001", or just "1").',
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
required: ['id'],
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: 'get_roadmap',
|
|
331
|
+
description:
|
|
332
|
+
'Reads the current roadmap content from ROADMAP.md. Returns milestones, phases, and planned work.',
|
|
333
|
+
inputSchema: {
|
|
334
|
+
type: 'object',
|
|
335
|
+
properties: {
|
|
336
|
+
section: {
|
|
337
|
+
type: 'string',
|
|
338
|
+
description: 'Optional: Return only a specific section/milestone.',
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
},
|
|
314
343
|
];
|
|
315
344
|
|
|
316
345
|
/**
|
|
@@ -1151,6 +1180,102 @@ async function manageProjectFile(args) {
|
|
|
1151
1180
|
}
|
|
1152
1181
|
}
|
|
1153
1182
|
|
|
1183
|
+
/**
|
|
1184
|
+
* Get decision handler
|
|
1185
|
+
*/
|
|
1186
|
+
async function getDecision(args) {
|
|
1187
|
+
const { id } = args;
|
|
1188
|
+
await ensureProjectDir();
|
|
1189
|
+
|
|
1190
|
+
const decisionsPath = join(PROJECT_DIR, 'DECISIONS.md');
|
|
1191
|
+
if (!(await fileExists(decisionsPath))) {
|
|
1192
|
+
return {
|
|
1193
|
+
content: [{ type: 'text', text: `❌ DECISIONS.md not found.` }],
|
|
1194
|
+
isError: true,
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
const content = await readFile(decisionsPath, 'utf-8');
|
|
1199
|
+
|
|
1200
|
+
// Normalize ID (handle "ADR-001", "001", or "1")
|
|
1201
|
+
let searchId = id.toUpperCase();
|
|
1202
|
+
if (!searchId.startsWith('ADR-')) {
|
|
1203
|
+
const num = parseInt(searchId.replace(/\D/g, ''));
|
|
1204
|
+
searchId = `ADR-${String(num).padStart(3, '0')}`;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// Find the decision section
|
|
1208
|
+
const decisionRegex = new RegExp(
|
|
1209
|
+
`## ${searchId}: ([^\\n]+)\\n\\n([\\s\\S]*?)(?=\\n## ADR-|\\n---\\n|$)`,
|
|
1210
|
+
'i'
|
|
1211
|
+
);
|
|
1212
|
+
const match = content.match(decisionRegex);
|
|
1213
|
+
|
|
1214
|
+
if (!match) {
|
|
1215
|
+
return {
|
|
1216
|
+
content: [{ type: 'text', text: `❌ Decision ${searchId} not found in DECISIONS.md` }],
|
|
1217
|
+
isError: true,
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
const title = match[1];
|
|
1222
|
+
const body = match[2].trim();
|
|
1223
|
+
|
|
1224
|
+
let result = `## ${searchId}: ${title}\n\n`;
|
|
1225
|
+
result += body;
|
|
1226
|
+
|
|
1227
|
+
return {
|
|
1228
|
+
content: [{ type: 'text', text: result }],
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Get roadmap handler
|
|
1234
|
+
*/
|
|
1235
|
+
async function getRoadmap(args) {
|
|
1236
|
+
const { section } = args || {};
|
|
1237
|
+
await ensureProjectDir();
|
|
1238
|
+
|
|
1239
|
+
const roadmapPath = join(PROJECT_DIR, 'ROADMAP.md');
|
|
1240
|
+
if (!(await fileExists(roadmapPath))) {
|
|
1241
|
+
return {
|
|
1242
|
+
content: [
|
|
1243
|
+
{
|
|
1244
|
+
type: 'text',
|
|
1245
|
+
text: `⚠️ ROADMAP.md not found. Use \`create_or_update_roadmap\` or \`add_roadmap_milestone\` to create it.`,
|
|
1246
|
+
},
|
|
1247
|
+
],
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
const content = await readFile(roadmapPath, 'utf-8');
|
|
1252
|
+
|
|
1253
|
+
if (section) {
|
|
1254
|
+
// Find specific section
|
|
1255
|
+
const sectionRegex = new RegExp(
|
|
1256
|
+
`## [^\\n]*${section.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^\\n]*\\n([\\s\\S]*?)(?=\\n## |\\n---\\n|$)`,
|
|
1257
|
+
'i'
|
|
1258
|
+
);
|
|
1259
|
+
const match = content.match(sectionRegex);
|
|
1260
|
+
|
|
1261
|
+
if (!match) {
|
|
1262
|
+
return {
|
|
1263
|
+
content: [{ type: 'text', text: `❌ Section "${section}" not found in ROADMAP.md` }],
|
|
1264
|
+
isError: true,
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
return {
|
|
1269
|
+
content: [{ type: 'text', text: match[0].trim() }],
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// Return full roadmap
|
|
1274
|
+
return {
|
|
1275
|
+
content: [{ type: 'text', text: content }],
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1154
1279
|
/**
|
|
1155
1280
|
* Handler map
|
|
1156
1281
|
*/
|
|
@@ -1164,6 +1289,8 @@ export const handlers = {
|
|
|
1164
1289
|
create_or_update_decisions: createOrUpdateDecisions,
|
|
1165
1290
|
add_decision: addDecision,
|
|
1166
1291
|
list_decisions: listDecisions,
|
|
1292
|
+
get_decision: getDecision,
|
|
1167
1293
|
update_project_status: updateProjectStatus,
|
|
1168
1294
|
add_roadmap_milestone: addRoadmapMilestone,
|
|
1295
|
+
get_roadmap: getRoadmap,
|
|
1169
1296
|
};
|
package/src/tools/tasks.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Task management tools.
|
|
3
|
-
* Handles: create_task, update_task, get_task, delete_task, get_next_task, list_tasks, sync_todo_index
|
|
3
|
+
* Handles: create_task, update_task, get_task, delete_task, get_next_task, list_tasks, search_tasks, sync_todo_index
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import {
|
|
@@ -269,6 +269,40 @@ export const definitions = [
|
|
|
269
269
|
},
|
|
270
270
|
},
|
|
271
271
|
},
|
|
272
|
+
{
|
|
273
|
+
name: 'search_tasks',
|
|
274
|
+
description:
|
|
275
|
+
'Search tasks by keyword in title, description, or content. Returns matching tasks with relevance ranking.',
|
|
276
|
+
inputSchema: {
|
|
277
|
+
type: 'object',
|
|
278
|
+
properties: {
|
|
279
|
+
query: {
|
|
280
|
+
type: 'string',
|
|
281
|
+
description: 'Search query (matches title, description, content).',
|
|
282
|
+
},
|
|
283
|
+
project: {
|
|
284
|
+
type: 'string',
|
|
285
|
+
description: 'Filter by project.',
|
|
286
|
+
},
|
|
287
|
+
status: {
|
|
288
|
+
type: 'string',
|
|
289
|
+
description: 'Filter by status.',
|
|
290
|
+
enum: ['todo', 'in_progress', 'blocked', 'review', 'done', ''],
|
|
291
|
+
},
|
|
292
|
+
include_archived: {
|
|
293
|
+
type: 'boolean',
|
|
294
|
+
description: 'Include archived tasks in search. Default: false.',
|
|
295
|
+
default: false,
|
|
296
|
+
},
|
|
297
|
+
limit: {
|
|
298
|
+
type: 'number',
|
|
299
|
+
description: 'Maximum results to return. Default: 10.',
|
|
300
|
+
default: 10,
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
required: ['query'],
|
|
304
|
+
},
|
|
305
|
+
},
|
|
272
306
|
{
|
|
273
307
|
name: 'sync_todo_index',
|
|
274
308
|
description:
|
|
@@ -704,6 +738,88 @@ async function listTasks(args) {
|
|
|
704
738
|
};
|
|
705
739
|
}
|
|
706
740
|
|
|
741
|
+
/**
|
|
742
|
+
* Search tasks handler
|
|
743
|
+
*/
|
|
744
|
+
async function searchTasks(args) {
|
|
745
|
+
const { query, project, status, include_archived = false, limit = 10 } = args;
|
|
746
|
+
|
|
747
|
+
await ensureTodosDir();
|
|
748
|
+
|
|
749
|
+
// Load active tasks
|
|
750
|
+
let tasks = await loadAllTasks();
|
|
751
|
+
|
|
752
|
+
// Load archived tasks if requested
|
|
753
|
+
if (include_archived) {
|
|
754
|
+
const { ARCHIVE_DIR } = await import('../lib/constants.js');
|
|
755
|
+
const { readdir } = await import('fs/promises');
|
|
756
|
+
try {
|
|
757
|
+
const archiveFiles = await readdir(ARCHIVE_DIR);
|
|
758
|
+
for (const file of archiveFiles) {
|
|
759
|
+
if (file.endsWith('.md')) {
|
|
760
|
+
const filePath = join(ARCHIVE_DIR, file);
|
|
761
|
+
const content = await readFile(filePath, 'utf-8');
|
|
762
|
+
const parsed = matter(content);
|
|
763
|
+
tasks.push({
|
|
764
|
+
...parsed.data,
|
|
765
|
+
content: parsed.content,
|
|
766
|
+
path: `archive/${file}`,
|
|
767
|
+
archived: true,
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
} catch {
|
|
772
|
+
// Archive dir may not exist
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Apply filters
|
|
777
|
+
if (project) {
|
|
778
|
+
tasks = tasks.filter((t) => t.project === project.toUpperCase());
|
|
779
|
+
}
|
|
780
|
+
if (status) {
|
|
781
|
+
tasks = tasks.filter((t) => t.status === status);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Search by query
|
|
785
|
+
const queryLower = query.toLowerCase();
|
|
786
|
+
const matches = tasks.filter((task) => {
|
|
787
|
+
const titleMatch = task.title?.toLowerCase().includes(queryLower);
|
|
788
|
+
const contentMatch = task.content?.toLowerCase().includes(queryLower);
|
|
789
|
+
const descMatch = task.description?.toLowerCase().includes(queryLower);
|
|
790
|
+
const tagMatch = task.tags?.some((t) => t.toLowerCase().includes(queryLower));
|
|
791
|
+
return titleMatch || contentMatch || descMatch || tagMatch;
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
// Sort by relevance (title matches first, then content)
|
|
795
|
+
matches.sort((a, b) => {
|
|
796
|
+
const aTitle = a.title?.toLowerCase().includes(queryLower) ? 1 : 0;
|
|
797
|
+
const bTitle = b.title?.toLowerCase().includes(queryLower) ? 1 : 0;
|
|
798
|
+
if (aTitle !== bTitle) return bTitle - aTitle;
|
|
799
|
+
return a.id.localeCompare(b.id);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
const results = matches.slice(0, limit);
|
|
803
|
+
|
|
804
|
+
let result = `## Search Results: "${query}"\n\n`;
|
|
805
|
+
result += `**Found:** ${matches.length} task(s)${matches.length > limit ? ` (showing ${limit})` : ''}\n\n`;
|
|
806
|
+
|
|
807
|
+
if (results.length === 0) {
|
|
808
|
+
result += `*No tasks found matching "${query}"*\n`;
|
|
809
|
+
} else {
|
|
810
|
+
result += `| ID | Title | Status | Priority | Location |\n`;
|
|
811
|
+
result += `|----|-------|--------|----------|----------|\n`;
|
|
812
|
+
for (const task of results) {
|
|
813
|
+
const location = task.archived ? '📦 archive' : '📋 active';
|
|
814
|
+
result += `| ${task.id} | ${task.title?.substring(0, 35)}${task.title?.length > 35 ? '...' : ''} | ${task.status} | ${task.priority} | ${location} |\n`;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return {
|
|
819
|
+
content: [{ type: 'text', text: result }],
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
707
823
|
/**
|
|
708
824
|
* Sync todo index handler
|
|
709
825
|
*/
|
|
@@ -822,5 +938,6 @@ export const handlers = {
|
|
|
822
938
|
delete_task: deleteTask,
|
|
823
939
|
get_next_task: getNextTask,
|
|
824
940
|
list_tasks: listTasks,
|
|
941
|
+
search_tasks: searchTasks,
|
|
825
942
|
sync_todo_index: syncTodoIndex,
|
|
826
943
|
};
|