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 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 | Use When |
139
- | ----------------- | ---------------------------------------------------- | ------------------------------- |
140
- | `import_tasks` | Parse plan/roadmap and add to BACKLOG.md | Populating the backlog |
141
- | `promote_task` | Move task from BACKLOG to active work (creates YAML) | Starting work on a backlog item |
142
- | `create_task` | Create active task directly (bypass backlog) | Urgent/immediate work |
143
- | `update_task` | Update any task field, transition status | Modifying existing tasks |
144
- | `get_next_task` | Get dependency-aware next task(s) to work on | Determining what to do next |
145
- | `list_tasks` | List/filter tasks with summary dashboard | Reviewing all tasks |
146
- | `archive_task` | Move completed task to archive/ | Cleaning up done work |
147
- | `sync_todo_index` | Generate TODO.md dashboard from active tasks | Updating the overview |
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 v1.3.0](docs/releases/RELEASE_NOTES_v1.3.0.md)** — Latest
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.1.0",
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",
@@ -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
  };
@@ -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
  };