backlog-mcp 0.28.0 → 0.29.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 +38 -6
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/operations/index.d.mts +6 -0
- package/dist/operations/index.mjs +7 -0
- package/dist/operations/logger.d.mts +23 -0
- package/dist/operations/logger.mjs +53 -0
- package/dist/operations/logger.mjs.map +1 -0
- package/dist/operations/middleware.d.mts +11 -0
- package/dist/operations/middleware.mjs +23 -0
- package/dist/operations/middleware.mjs.map +1 -0
- package/dist/operations/resource-id.d.mts +12 -0
- package/dist/operations/resource-id.mjs +24 -0
- package/dist/operations/resource-id.mjs.map +1 -0
- package/dist/operations/storage.d.mts +26 -0
- package/dist/operations/storage.mjs +64 -0
- package/dist/operations/storage.mjs.map +1 -0
- package/dist/operations/types.d.mts +27 -0
- package/dist/operations/types.mjs +11 -0
- package/dist/operations/types.mjs.map +1 -0
- package/dist/resources/manager.d.mts +7 -0
- package/dist/resources/manager.mjs +38 -2
- package/dist/resources/manager.mjs.map +1 -1
- package/dist/search/embedding-service.d.mts +27 -0
- package/dist/search/embedding-service.mjs +47 -0
- package/dist/search/embedding-service.mjs.map +1 -0
- package/dist/search/index.d.mts +4 -0
- package/dist/search/index.mjs +4 -0
- package/dist/search/orama-search-service.d.mts +76 -0
- package/dist/search/orama-search-service.mjs +526 -0
- package/dist/search/orama-search-service.mjs.map +1 -0
- package/dist/search/types.d.mts +81 -0
- package/dist/search/types.mjs +1 -0
- package/dist/server/mcp-handler.mjs +4 -2
- package/dist/server/mcp-handler.mjs.map +1 -1
- package/dist/server/viewer-routes.mjs +29 -4
- package/dist/server/viewer-routes.mjs.map +1 -1
- package/dist/storage/backlog-service.d.mts +49 -0
- package/dist/storage/backlog-service.mjs +99 -0
- package/dist/storage/backlog-service.mjs.map +1 -0
- package/dist/storage/{backlog.d.mts → task-storage.d.mts} +8 -8
- package/dist/storage/{backlog.mjs → task-storage.mjs} +8 -11
- package/dist/storage/task-storage.mjs.map +1 -0
- package/dist/tools/backlog-create.mjs +1 -1
- package/dist/tools/backlog-create.mjs.map +1 -1
- package/dist/tools/backlog-delete.mjs +1 -1
- package/dist/tools/backlog-delete.mjs.map +1 -1
- package/dist/tools/backlog-get.mjs +1 -1
- package/dist/tools/backlog-get.mjs.map +1 -1
- package/dist/tools/backlog-list.mjs +6 -4
- package/dist/tools/backlog-list.mjs.map +1 -1
- package/dist/tools/backlog-update.mjs +2 -3
- package/dist/tools/backlog-update.mjs.map +1 -1
- package/dist/utils/logger.mjs +1 -1
- package/dist/utils/paths.mjs +1 -1
- package/dist/viewer/activity-COWPF622.svg +4 -0
- package/dist/viewer/index.html +12 -1
- package/dist/viewer/main.css +1292 -39
- package/dist/viewer/main.js +3803 -270
- package/package.json +6 -1
- package/viewer/components/activity-panel.ts +326 -0
- package/viewer/components/spotlight-search.ts +429 -0
- package/viewer/components/task-badge.ts +2 -2
- package/viewer/components/task-detail.ts +33 -6
- package/viewer/components/task-filter-bar.ts +13 -1
- package/viewer/components/task-item.ts +1 -1
- package/viewer/components/task-list.ts +10 -2
- package/viewer/icons/activity.svg +4 -0
- package/viewer/icons/index.ts +2 -0
- package/viewer/index.html +12 -1
- package/viewer/main.ts +36 -16
- package/viewer/styles.css +633 -0
- package/viewer/utils/api.ts +9 -2
- package/viewer/utils/split-pane.ts +113 -8
- package/viewer/utils/url-state.ts +2 -0
- package/dist/storage/backlog.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -63,6 +63,7 @@ backlog_list # List active tasks (open, in_progress
|
|
|
63
63
|
backlog_list status=["done"] # Show completed tasks
|
|
64
64
|
backlog_list type="epic" # List only epics
|
|
65
65
|
backlog_list epic_id="EPIC-0002" # Tasks in specific epic
|
|
66
|
+
backlog_list query="authentication" # Search across all fields
|
|
66
67
|
backlog_list counts=true # Get counts by status
|
|
67
68
|
backlog_list limit=10 # Limit results
|
|
68
69
|
|
|
@@ -77,20 +78,51 @@ backlog_update id="TASK-0001" status="done" # Mark done
|
|
|
77
78
|
backlog_update id="TASK-0001" status="blocked" blocked_reason="Waiting on API"
|
|
78
79
|
backlog_update id="TASK-0001" evidence=["Fixed in CR-12345"] # Add completion proof
|
|
79
80
|
|
|
81
|
+
# To update task content, use write_resource:
|
|
82
|
+
write_resource uri="mcp://backlog/tasks/TASK-0001.md" operation={type: "str_replace", old_str: "old text", new_str: "new text"}
|
|
83
|
+
|
|
80
84
|
backlog_delete id="TASK-0001" # Permanently delete
|
|
81
85
|
```
|
|
82
86
|
|
|
87
|
+
### Search
|
|
88
|
+
|
|
89
|
+
Search across all task fields (title, description, evidence, references, blocked_reason, epic_id):
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
backlog_list query="oauth" # Find tasks mentioning OAuth
|
|
93
|
+
backlog_list query="bug" status=["open"] # Search within open tasks
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Search is case-insensitive substring matching. Works with all other filters.
|
|
97
|
+
|
|
83
98
|
### Resources (MCP Resources Protocol)
|
|
84
99
|
|
|
85
100
|
Access tasks and resources via MCP resource URIs:
|
|
86
101
|
|
|
87
102
|
```
|
|
88
103
|
mcp://backlog/tasks/TASK-0001.md # Task markdown file
|
|
89
|
-
mcp://backlog/resources/
|
|
90
|
-
|
|
104
|
+
mcp://backlog/resources/path/to/file.md # Standalone resource
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### write_resource Tool
|
|
108
|
+
|
|
109
|
+
Create and edit files on the MCP server. Operations mirror `fs_write`:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
write_resource uri="mcp://backlog/resources/notes.md" operation={type: "create", file_text: "# Notes\n\nContent here"}
|
|
113
|
+
|
|
114
|
+
write_resource uri="mcp://backlog/resources/notes.md" operation={type: "str_replace", old_str: "old text", new_str: "new text"}
|
|
115
|
+
|
|
116
|
+
write_resource uri="mcp://backlog/resources/notes.md" operation={type: "insert", insert_line: 5, new_str: "inserted line"}
|
|
117
|
+
|
|
118
|
+
write_resource uri="mcp://backlog/resources/notes.md" operation={type: "append", new_str: "appended content"}
|
|
91
119
|
```
|
|
92
120
|
|
|
93
|
-
|
|
121
|
+
**Operations:**
|
|
122
|
+
- `create` - Create file or override existing (file_text required)
|
|
123
|
+
- `str_replace` - Replace exact string match (must be unique in file)
|
|
124
|
+
- `insert` - Insert after specified line number
|
|
125
|
+
- `append` - Add to end of file (auto-adds newline if needed)
|
|
94
126
|
|
|
95
127
|
## Installation
|
|
96
128
|
|
|
@@ -122,7 +154,7 @@ pnpm start
|
|
|
122
154
|
backlog-mcp # Run as stdio MCP server (default)
|
|
123
155
|
backlog-mcp serve # Run HTTP server with viewer
|
|
124
156
|
backlog-mcp version # Show version
|
|
125
|
-
backlog-mcp status # Check
|
|
157
|
+
backlog-mcp status # Check server status (port, version, task count, uptime)
|
|
126
158
|
backlog-mcp stop # Stop the server
|
|
127
159
|
backlog-mcp --help # Show help
|
|
128
160
|
```
|
|
@@ -151,9 +183,9 @@ BACKLOG_VIEWER_PORT=3030 # HTTP server port
|
|
|
151
183
|
|
|
152
184
|
## Storage
|
|
153
185
|
|
|
154
|
-
- Default: `data/tasks/`
|
|
186
|
+
- Default: `data/tasks/` (local to project)
|
|
155
187
|
- Global: Set `BACKLOG_DATA_DIR=~/.backlog` for cross-project persistence
|
|
156
|
-
-
|
|
188
|
+
- All tasks stored in single `tasks/` directory regardless of status (see [ADR 0003](docs/adr/0003-remove-archive-directory.md))
|
|
157
189
|
|
|
158
190
|
## License
|
|
159
191
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { CreateTaskInput, STATUSES, Status, Task, createTask, formatTaskId, isValidTaskId, nextTaskId, parseTaskId } from "./storage/schema.mjs";
|
|
2
|
-
import { storage } from "./storage/backlog.mjs";
|
|
2
|
+
import { storage } from "./storage/backlog-service.mjs";
|
|
3
3
|
import { startHttpServer } from "./server/fastify-server.mjs";
|
|
4
4
|
export { type CreateTaskInput, STATUSES, type Status, type Task, createTask, formatTaskId, isValidTaskId, nextTaskId, parseTaskId, startHttpServer, storage };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { STATUSES, createTask, formatTaskId, isValidTaskId, nextTaskId, parseTaskId } from "./storage/schema.mjs";
|
|
2
|
-
import { storage } from "./storage/backlog.mjs";
|
|
2
|
+
import { storage } from "./storage/backlog-service.mjs";
|
|
3
3
|
import { startHttpServer } from "./server/fastify-server.mjs";
|
|
4
4
|
|
|
5
5
|
export { STATUSES, createTask, formatTaskId, isValidTaskId, nextTaskId, parseTaskId, startHttpServer, storage };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Actor, OperationEntry, OperationFilter, ToolName, WRITE_TOOLS } from "./types.mjs";
|
|
2
|
+
import { operationLogger } from "./logger.mjs";
|
|
3
|
+
import { withOperationLogging } from "./middleware.mjs";
|
|
4
|
+
import { OperationStorage } from "./storage.mjs";
|
|
5
|
+
import { extractResourceId } from "./resource-id.mjs";
|
|
6
|
+
export { type Actor, type OperationEntry, type OperationFilter, OperationStorage, type ToolName, WRITE_TOOLS, extractResourceId, operationLogger, withOperationLogging };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { OperationStorage } from "./storage.mjs";
|
|
2
|
+
import { extractResourceId } from "./resource-id.mjs";
|
|
3
|
+
import { WRITE_TOOLS } from "./types.mjs";
|
|
4
|
+
import { operationLogger } from "./logger.mjs";
|
|
5
|
+
import { withOperationLogging } from "./middleware.mjs";
|
|
6
|
+
|
|
7
|
+
export { OperationStorage, WRITE_TOOLS, extractResourceId, operationLogger, withOperationLogging };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Actor, OperationEntry, OperationFilter } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/operations/logger.d.ts
|
|
4
|
+
declare class OperationLogger {
|
|
5
|
+
private storage;
|
|
6
|
+
constructor();
|
|
7
|
+
/**
|
|
8
|
+
* Log a tool operation. Only logs write operations.
|
|
9
|
+
*/
|
|
10
|
+
log(tool: string, params: Record<string, unknown>, result: unknown): void;
|
|
11
|
+
/**
|
|
12
|
+
* Read recent operations, optionally filtered by task ID.
|
|
13
|
+
*/
|
|
14
|
+
read(options?: OperationFilter): OperationEntry[];
|
|
15
|
+
/**
|
|
16
|
+
* Count operations for a specific task (for badge display).
|
|
17
|
+
*/
|
|
18
|
+
countForTask(taskId: string): number;
|
|
19
|
+
}
|
|
20
|
+
declare const operationLogger: OperationLogger;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { type Actor, type OperationEntry, type OperationFilter, operationLogger };
|
|
23
|
+
//# sourceMappingURL=logger.d.mts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { OperationStorage } from "./storage.mjs";
|
|
2
|
+
import { extractResourceId } from "./resource-id.mjs";
|
|
3
|
+
import { WRITE_TOOLS } from "./types.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/operations/logger.ts
|
|
6
|
+
/**
|
|
7
|
+
* Operation logger - thin orchestration layer.
|
|
8
|
+
* Coordinates storage, resource ID extraction, and actor info.
|
|
9
|
+
*/
|
|
10
|
+
const actor = {
|
|
11
|
+
type: process.env.BACKLOG_ACTOR_TYPE || "user",
|
|
12
|
+
name: process.env.BACKLOG_ACTOR_NAME || process.env.USER || "unknown",
|
|
13
|
+
delegatedBy: process.env.BACKLOG_DELEGATED_BY,
|
|
14
|
+
taskContext: process.env.BACKLOG_TASK_CONTEXT
|
|
15
|
+
};
|
|
16
|
+
var OperationLogger = class {
|
|
17
|
+
storage;
|
|
18
|
+
constructor() {
|
|
19
|
+
this.storage = new OperationStorage();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Log a tool operation. Only logs write operations.
|
|
23
|
+
*/
|
|
24
|
+
log(tool, params, result) {
|
|
25
|
+
if (!WRITE_TOOLS.includes(tool)) return;
|
|
26
|
+
const entry = {
|
|
27
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28
|
+
tool,
|
|
29
|
+
params,
|
|
30
|
+
result,
|
|
31
|
+
resourceId: extractResourceId(tool, params, result),
|
|
32
|
+
actor
|
|
33
|
+
};
|
|
34
|
+
this.storage.append(entry);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Read recent operations, optionally filtered by task ID.
|
|
38
|
+
*/
|
|
39
|
+
read(options = {}) {
|
|
40
|
+
return this.storage.query(options);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Count operations for a specific task (for badge display).
|
|
44
|
+
*/
|
|
45
|
+
countForTask(taskId) {
|
|
46
|
+
return this.storage.countForTask(taskId);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const operationLogger = new OperationLogger();
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { operationLogger };
|
|
53
|
+
//# sourceMappingURL=logger.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.mjs","names":[],"sources":["../../src/operations/logger.ts"],"sourcesContent":["/**\n * Operation logger - thin orchestration layer.\n * Coordinates storage, resource ID extraction, and actor info.\n */\n\nimport { OperationStorage } from './storage.js';\nimport { extractResourceId } from './resource-id.js';\nimport type { Actor, OperationEntry, OperationFilter } from './types.js';\nimport { WRITE_TOOLS } from './types.js';\n\n// Read actor info from environment at module load\nconst actor: Actor = {\n type: (process.env.BACKLOG_ACTOR_TYPE as 'user' | 'agent') || 'user',\n name: process.env.BACKLOG_ACTOR_NAME || process.env.USER || 'unknown',\n delegatedBy: process.env.BACKLOG_DELEGATED_BY,\n taskContext: process.env.BACKLOG_TASK_CONTEXT,\n};\n\nclass OperationLogger {\n private storage: OperationStorage;\n\n constructor() {\n this.storage = new OperationStorage();\n }\n\n /**\n * Log a tool operation. Only logs write operations.\n */\n log(tool: string, params: Record<string, unknown>, result: unknown): void {\n if (!WRITE_TOOLS.includes(tool as any)) return;\n\n const entry: OperationEntry = {\n ts: new Date().toISOString(),\n tool,\n params,\n result,\n resourceId: extractResourceId(tool, params, result),\n actor,\n };\n\n this.storage.append(entry);\n }\n\n /**\n * Read recent operations, optionally filtered by task ID.\n */\n read(options: OperationFilter = {}): OperationEntry[] {\n return this.storage.query(options);\n }\n\n /**\n * Count operations for a specific task (for badge display).\n */\n countForTask(taskId: string): number {\n return this.storage.countForTask(taskId);\n }\n}\n\nexport const operationLogger = new OperationLogger();\n\n// Re-export types for convenience\nexport type { Actor, OperationEntry, OperationFilter } from './types.js';\n"],"mappings":";;;;;;;;;AAWA,MAAM,QAAe;CACnB,MAAO,QAAQ,IAAI,sBAA2C;CAC9D,MAAM,QAAQ,IAAI,sBAAsB,QAAQ,IAAI,QAAQ;CAC5D,aAAa,QAAQ,IAAI;CACzB,aAAa,QAAQ,IAAI;CAC1B;AAED,IAAM,kBAAN,MAAsB;CACpB,AAAQ;CAER,cAAc;AACZ,OAAK,UAAU,IAAI,kBAAkB;;;;;CAMvC,IAAI,MAAc,QAAiC,QAAuB;AACxE,MAAI,CAAC,YAAY,SAAS,KAAY,CAAE;EAExC,MAAM,QAAwB;GAC5B,qBAAI,IAAI,MAAM,EAAC,aAAa;GAC5B;GACA;GACA;GACA,YAAY,kBAAkB,MAAM,QAAQ,OAAO;GACnD;GACD;AAED,OAAK,QAAQ,OAAO,MAAM;;;;;CAM5B,KAAK,UAA2B,EAAE,EAAoB;AACpD,SAAO,KAAK,QAAQ,MAAM,QAAQ;;;;;CAMpC,aAAa,QAAwB;AACnC,SAAO,KAAK,QAAQ,aAAa,OAAO;;;AAI5C,MAAa,kBAAkB,IAAI,iBAAiB"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
|
|
3
|
+
//#region src/operations/middleware.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Wrap an MCP server to log tool operations.
|
|
6
|
+
* Returns a proxy that intercepts registerTool calls.
|
|
7
|
+
*/
|
|
8
|
+
declare function withOperationLogging(server: McpServer): McpServer;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { withOperationLogging };
|
|
11
|
+
//# sourceMappingURL=middleware.d.mts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { operationLogger } from "./logger.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/operations/middleware.ts
|
|
4
|
+
/**
|
|
5
|
+
* Wrap an MCP server to log tool operations.
|
|
6
|
+
* Returns a proxy that intercepts registerTool calls.
|
|
7
|
+
*/
|
|
8
|
+
function withOperationLogging(server) {
|
|
9
|
+
const originalRegisterTool = server.registerTool.bind(server);
|
|
10
|
+
server.registerTool = function(name, config, callback) {
|
|
11
|
+
const wrappedCallback = async (...args) => {
|
|
12
|
+
const result = await callback(...args);
|
|
13
|
+
operationLogger.log(name, args[0] || {}, result);
|
|
14
|
+
return result;
|
|
15
|
+
};
|
|
16
|
+
return originalRegisterTool(name, config, wrappedCallback);
|
|
17
|
+
};
|
|
18
|
+
return server;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
export { withOperationLogging };
|
|
23
|
+
//# sourceMappingURL=middleware.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.mjs","names":[],"sources":["../../src/operations/middleware.ts"],"sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { operationLogger } from './logger.js';\n\n/**\n * Wrap an MCP server to log tool operations.\n * Returns a proxy that intercepts registerTool calls.\n */\nexport function withOperationLogging(server: McpServer): McpServer {\n const originalRegisterTool = server.registerTool.bind(server);\n\n // Override registerTool to wrap callbacks with logging\n (server as any).registerTool = function(\n name: string,\n config: any,\n callback: (...args: any[]) => any\n ) {\n const wrappedCallback = async (...args: any[]) => {\n const result = await callback(...args);\n // args[0] is the params object for tool callbacks\n operationLogger.log(name, args[0] || {}, result);\n return result;\n };\n\n return originalRegisterTool(name, config, wrappedCallback as any);\n };\n\n return server;\n}\n"],"mappings":";;;;;;;AAOA,SAAgB,qBAAqB,QAA8B;CACjE,MAAM,uBAAuB,OAAO,aAAa,KAAK,OAAO;AAG7D,CAAC,OAAe,eAAe,SAC7B,MACA,QACA,UACA;EACA,MAAM,kBAAkB,OAAO,GAAG,SAAgB;GAChD,MAAM,SAAS,MAAM,SAAS,GAAG,KAAK;AAEtC,mBAAgB,IAAI,MAAM,KAAK,MAAM,EAAE,EAAE,OAAO;AAChD,UAAO;;AAGT,SAAO,qBAAqB,MAAM,QAAQ,gBAAuB;;AAGnE,QAAO"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/operations/resource-id.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Resource ID extraction from tool params/results.
|
|
4
|
+
* Each tool has its own extraction strategy.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Extract resource ID from tool params or result for filtering.
|
|
8
|
+
*/
|
|
9
|
+
declare function extractResourceId(tool: string, params: Record<string, unknown>, result: unknown): string | undefined;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { extractResourceId };
|
|
12
|
+
//# sourceMappingURL=resource-id.d.mts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/operations/resource-id.ts
|
|
2
|
+
const extractors = {
|
|
3
|
+
backlog_create: (_, result) => {
|
|
4
|
+
const text = result?.content?.[0]?.text;
|
|
5
|
+
if (text) return text.match(/(TASK|EPIC)-\d+/)?.[0];
|
|
6
|
+
},
|
|
7
|
+
backlog_update: (params) => params.id,
|
|
8
|
+
backlog_delete: (params) => params.id,
|
|
9
|
+
write_resource: (params) => {
|
|
10
|
+
const uri = params.uri;
|
|
11
|
+
if (uri) return uri.match(/(TASK|EPIC)-\d+/)?.[0];
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Extract resource ID from tool params or result for filtering.
|
|
16
|
+
*/
|
|
17
|
+
function extractResourceId(tool, params, result) {
|
|
18
|
+
const extractor = extractors[tool];
|
|
19
|
+
return extractor ? extractor(params, result) : void 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
export { extractResourceId };
|
|
24
|
+
//# sourceMappingURL=resource-id.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource-id.mjs","names":[],"sources":["../../src/operations/resource-id.ts"],"sourcesContent":["/**\n * Resource ID extraction from tool params/results.\n * Each tool has its own extraction strategy.\n */\n\ntype Extractor = (params: Record<string, unknown>, result: unknown) => string | undefined;\n\nconst extractors: Record<string, Extractor> = {\n backlog_create: (_, result) => {\n const text = (result as any)?.content?.[0]?.text as string | undefined;\n if (text) {\n const match = text.match(/(TASK|EPIC)-\\d+/);\n return match?.[0];\n }\n return undefined;\n },\n\n backlog_update: (params) => params.id as string | undefined,\n\n backlog_delete: (params) => params.id as string | undefined,\n\n write_resource: (params) => {\n const uri = params.uri as string | undefined;\n if (uri) {\n const match = uri.match(/(TASK|EPIC)-\\d+/);\n return match?.[0];\n }\n return undefined;\n },\n};\n\n/**\n * Extract resource ID from tool params or result for filtering.\n */\nexport function extractResourceId(\n tool: string,\n params: Record<string, unknown>,\n result: unknown\n): string | undefined {\n const extractor = extractors[tool];\n return extractor ? extractor(params, result) : undefined;\n}\n"],"mappings":";AAOA,MAAM,aAAwC;CAC5C,iBAAiB,GAAG,WAAW;EAC7B,MAAM,OAAQ,QAAgB,UAAU,IAAI;AAC5C,MAAI,KAEF,QADc,KAAK,MAAM,kBAAkB,GAC5B;;CAKnB,iBAAiB,WAAW,OAAO;CAEnC,iBAAiB,WAAW,OAAO;CAEnC,iBAAiB,WAAW;EAC1B,MAAM,MAAM,OAAO;AACnB,MAAI,IAEF,QADc,IAAI,MAAM,kBAAkB,GAC3B;;CAIpB;;;;AAKD,SAAgB,kBACd,MACA,QACA,QACoB;CACpB,MAAM,YAAY,WAAW;AAC7B,QAAO,YAAY,UAAU,QAAQ,OAAO,GAAG"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { OperationEntry, OperationFilter } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/operations/storage.d.ts
|
|
4
|
+
declare class OperationStorage {
|
|
5
|
+
private logPath;
|
|
6
|
+
constructor();
|
|
7
|
+
/**
|
|
8
|
+
* Append an operation entry to the log file.
|
|
9
|
+
*/
|
|
10
|
+
append(entry: OperationEntry): void;
|
|
11
|
+
/**
|
|
12
|
+
* Read all operations from the log file.
|
|
13
|
+
*/
|
|
14
|
+
readAll(): OperationEntry[];
|
|
15
|
+
/**
|
|
16
|
+
* Query operations with optional filtering.
|
|
17
|
+
*/
|
|
18
|
+
query(filter?: OperationFilter): OperationEntry[];
|
|
19
|
+
/**
|
|
20
|
+
* Count operations for a specific task.
|
|
21
|
+
*/
|
|
22
|
+
countForTask(taskId: string): number;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { OperationStorage };
|
|
26
|
+
//# sourceMappingURL=storage.d.mts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { paths } from "../utils/paths.mjs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
//#region src/operations/storage.ts
|
|
6
|
+
/**
|
|
7
|
+
* JSONL storage for operation entries.
|
|
8
|
+
* Single responsibility: read/write operations to disk.
|
|
9
|
+
*/
|
|
10
|
+
var OperationStorage = class {
|
|
11
|
+
logPath;
|
|
12
|
+
constructor() {
|
|
13
|
+
this.logPath = join(paths.backlogDataDir, ".internal", "operations.jsonl");
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Append an operation entry to the log file.
|
|
17
|
+
*/
|
|
18
|
+
append(entry) {
|
|
19
|
+
try {
|
|
20
|
+
const dir = dirname(this.logPath);
|
|
21
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
22
|
+
appendFileSync(this.logPath, JSON.stringify(entry) + "\n", "utf-8");
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Read all operations from the log file.
|
|
27
|
+
*/
|
|
28
|
+
readAll() {
|
|
29
|
+
if (!existsSync(this.logPath)) return [];
|
|
30
|
+
try {
|
|
31
|
+
return readFileSync(this.logPath, "utf-8").trim().split("\n").filter(Boolean).map((line) => {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(line);
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}).filter((e) => e !== null);
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Query operations with optional filtering.
|
|
44
|
+
*/
|
|
45
|
+
query(filter = {}) {
|
|
46
|
+
const { taskId, limit = 50 } = filter;
|
|
47
|
+
let entries = this.readAll();
|
|
48
|
+
if (taskId) entries = entries.filter((e) => e.resourceId === taskId);
|
|
49
|
+
return entries.reverse().slice(0, limit);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Count operations for a specific task.
|
|
53
|
+
*/
|
|
54
|
+
countForTask(taskId) {
|
|
55
|
+
return this.query({
|
|
56
|
+
taskId,
|
|
57
|
+
limit: 1e3
|
|
58
|
+
}).length;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
export { OperationStorage };
|
|
64
|
+
//# sourceMappingURL=storage.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.mjs","names":[],"sources":["../../src/operations/storage.ts"],"sourcesContent":["/**\n * JSONL storage for operation entries.\n * Single responsibility: read/write operations to disk.\n */\n\nimport { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { paths } from '@/utils/paths.js';\nimport type { OperationEntry, OperationFilter } from './types.js';\n\nexport class OperationStorage {\n private logPath: string;\n\n constructor() {\n this.logPath = join(paths.backlogDataDir, '.internal', 'operations.jsonl');\n }\n\n /**\n * Append an operation entry to the log file.\n */\n append(entry: OperationEntry): void {\n try {\n const dir = dirname(this.logPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n appendFileSync(this.logPath, JSON.stringify(entry) + '\\n', 'utf-8');\n } catch {\n // Fail silently - logging should not break tool execution\n }\n }\n\n /**\n * Read all operations from the log file.\n */\n readAll(): OperationEntry[] {\n if (!existsSync(this.logPath)) return [];\n\n try {\n const content = readFileSync(this.logPath, 'utf-8');\n const lines = content.trim().split('\\n').filter(Boolean);\n \n return lines\n .map(line => {\n try {\n return JSON.parse(line) as OperationEntry;\n } catch {\n return null;\n }\n })\n .filter((e): e is OperationEntry => e !== null);\n } catch {\n return [];\n }\n }\n\n /**\n * Query operations with optional filtering.\n */\n query(filter: OperationFilter = {}): OperationEntry[] {\n const { taskId, limit = 50 } = filter;\n \n let entries = this.readAll();\n\n if (taskId) {\n entries = entries.filter(e => e.resourceId === taskId);\n }\n\n // Return most recent first, limited\n return entries.reverse().slice(0, limit);\n }\n\n /**\n * Count operations for a specific task.\n */\n countForTask(taskId: string): number {\n return this.query({ taskId, limit: 1000 }).length;\n }\n}\n"],"mappings":";;;;;;;;;AAUA,IAAa,mBAAb,MAA8B;CAC5B,AAAQ;CAER,cAAc;AACZ,OAAK,UAAU,KAAK,MAAM,gBAAgB,aAAa,mBAAmB;;;;;CAM5E,OAAO,OAA6B;AAClC,MAAI;GACF,MAAM,MAAM,QAAQ,KAAK,QAAQ;AACjC,OAAI,CAAC,WAAW,IAAI,CAClB,WAAU,KAAK,EAAE,WAAW,MAAM,CAAC;AAErC,kBAAe,KAAK,SAAS,KAAK,UAAU,MAAM,GAAG,MAAM,QAAQ;UAC7D;;;;;CAQV,UAA4B;AAC1B,MAAI,CAAC,WAAW,KAAK,QAAQ,CAAE,QAAO,EAAE;AAExC,MAAI;AAIF,UAHgB,aAAa,KAAK,SAAS,QAAQ,CAC7B,MAAM,CAAC,MAAM,KAAK,CAAC,OAAO,QAAQ,CAGrD,KAAI,SAAQ;AACX,QAAI;AACF,YAAO,KAAK,MAAM,KAAK;YACjB;AACN,YAAO;;KAET,CACD,QAAQ,MAA2B,MAAM,KAAK;UAC3C;AACN,UAAO,EAAE;;;;;;CAOb,MAAM,SAA0B,EAAE,EAAoB;EACpD,MAAM,EAAE,QAAQ,QAAQ,OAAO;EAE/B,IAAI,UAAU,KAAK,SAAS;AAE5B,MAAI,OACF,WAAU,QAAQ,QAAO,MAAK,EAAE,eAAe,OAAO;AAIxD,SAAO,QAAQ,SAAS,CAAC,MAAM,GAAG,MAAM;;;;;CAM1C,aAAa,QAAwB;AACnC,SAAO,KAAK,MAAM;GAAE;GAAQ,OAAO;GAAM,CAAC,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/operations/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Types for operation logging.
|
|
4
|
+
*/
|
|
5
|
+
interface Actor {
|
|
6
|
+
type: 'user' | 'agent';
|
|
7
|
+
name: string;
|
|
8
|
+
delegatedBy?: string;
|
|
9
|
+
taskContext?: string;
|
|
10
|
+
}
|
|
11
|
+
type ToolName = 'backlog_create' | 'backlog_update' | 'backlog_delete' | 'write_resource';
|
|
12
|
+
interface OperationEntry {
|
|
13
|
+
ts: string;
|
|
14
|
+
tool: string;
|
|
15
|
+
params: Record<string, unknown>;
|
|
16
|
+
result: unknown;
|
|
17
|
+
resourceId?: string;
|
|
18
|
+
actor: Actor;
|
|
19
|
+
}
|
|
20
|
+
interface OperationFilter {
|
|
21
|
+
taskId?: string;
|
|
22
|
+
limit?: number;
|
|
23
|
+
}
|
|
24
|
+
declare const WRITE_TOOLS: ToolName[];
|
|
25
|
+
//#endregion
|
|
26
|
+
export { Actor, OperationEntry, OperationFilter, ToolName, WRITE_TOOLS };
|
|
27
|
+
//# sourceMappingURL=types.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.mjs","names":[],"sources":["../../src/operations/types.ts"],"sourcesContent":["/**\n * Types for operation logging.\n */\n\nexport interface Actor {\n type: 'user' | 'agent';\n name: string;\n delegatedBy?: string;\n taskContext?: string;\n}\n\nexport type ToolName = 'backlog_create' | 'backlog_update' | 'backlog_delete' | 'write_resource';\n\nexport interface OperationEntry {\n ts: string;\n tool: string;\n params: Record<string, unknown>;\n result: unknown;\n resourceId?: string;\n actor: Actor;\n}\n\nexport interface OperationFilter {\n taskId?: string;\n limit?: number;\n}\n\nexport const WRITE_TOOLS: ToolName[] = ['backlog_create', 'backlog_update', 'backlog_delete', 'write_resource'];\n"],"mappings":";AA2BA,MAAa,cAA0B;CAAC;CAAkB;CAAkB;CAAkB;CAAiB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Resource } from "../search/types.mjs";
|
|
1
2
|
import { Operation, WriteResourceResult } from "./types.mjs";
|
|
2
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
4
|
|
|
@@ -16,6 +17,12 @@ interface ResourceContent {
|
|
|
16
17
|
declare class ResourceManager {
|
|
17
18
|
private readonly dataDir;
|
|
18
19
|
constructor(dataDir: string);
|
|
20
|
+
/**
|
|
21
|
+
* List all resources in the resources/ directory.
|
|
22
|
+
* Returns Resource objects ready for search indexing.
|
|
23
|
+
*/
|
|
24
|
+
list(): Resource[];
|
|
25
|
+
private scanDirectory;
|
|
19
26
|
/**
|
|
20
27
|
* Resolve MCP URI to absolute file path.
|
|
21
28
|
* Pure catch-all: mcp://backlog/path/file.md → {dataDir}/path/file.md
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { paths } from "../utils/paths.mjs";
|
|
2
2
|
import { applyOperation } from "./operations.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { dirname, join, relative } from "node:path";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
import matter from "gray-matter";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
|
|
9
9
|
//#region src/resources/manager.ts
|
|
10
10
|
/**
|
|
11
|
+
* Extract title from markdown content.
|
|
12
|
+
* Returns first # heading or filename without extension.
|
|
13
|
+
*/
|
|
14
|
+
function extractTitle(content, filename) {
|
|
15
|
+
return content.match(/^#\s+(.+)$/m)?.[1]?.trim() || filename.replace(/\.md$/, "");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
11
18
|
* ResourceManager - Single point of responsibility for MCP resource operations.
|
|
12
19
|
*
|
|
13
20
|
* Pure catch-all design: mcp://backlog/{+path} → {dataDir}/{path}
|
|
@@ -18,6 +25,35 @@ var ResourceManager = class {
|
|
|
18
25
|
this.dataDir = dataDir;
|
|
19
26
|
}
|
|
20
27
|
/**
|
|
28
|
+
* List all resources in the resources/ directory.
|
|
29
|
+
* Returns Resource objects ready for search indexing.
|
|
30
|
+
*/
|
|
31
|
+
list() {
|
|
32
|
+
const resourcesDir = join(this.dataDir, "resources");
|
|
33
|
+
if (!existsSync(resourcesDir)) return [];
|
|
34
|
+
const resources = [];
|
|
35
|
+
this.scanDirectory(resourcesDir, resources);
|
|
36
|
+
return resources;
|
|
37
|
+
}
|
|
38
|
+
scanDirectory(dir, resources) {
|
|
39
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const fullPath = join(dir, entry.name);
|
|
42
|
+
if (entry.isDirectory()) this.scanDirectory(fullPath, resources);
|
|
43
|
+
else if (entry.isFile() && entry.name.endsWith(".md")) try {
|
|
44
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
45
|
+
const relativePath = relative(this.dataDir, fullPath);
|
|
46
|
+
const uri = `mcp://backlog/${relativePath}`;
|
|
47
|
+
resources.push({
|
|
48
|
+
id: uri,
|
|
49
|
+
path: relativePath,
|
|
50
|
+
title: extractTitle(content, entry.name),
|
|
51
|
+
content
|
|
52
|
+
});
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
21
57
|
* Resolve MCP URI to absolute file path.
|
|
22
58
|
* Pure catch-all: mcp://backlog/path/file.md → {dataDir}/path/file.md
|
|
23
59
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.mjs","names":[],"sources":["../../src/resources/manager.ts"],"sourcesContent":["import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport matter from 'gray-matter';\nimport { z } from 'zod';\nimport { paths } from '@/utils/paths.js';\nimport { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { Operation, WriteResourceResult } from './types.js';\nimport { applyOperation } from './operations.js';\n\nexport interface ResourceContent {\n content: string;\n frontmatter?: Record<string, any>;\n mimeType: string;\n}\n\n/**\n * ResourceManager - Single point of responsibility for MCP resource operations.\n * \n * Pure catch-all design: mcp://backlog/{+path} → {dataDir}/{path}\n * No special cases, no magic behavior.\n */\nexport class ResourceManager {\n constructor(private readonly dataDir: string) {}\n\n /**\n * Resolve MCP URI to absolute file path.\n * Pure catch-all: mcp://backlog/path/file.md → {dataDir}/path/file.md\n * \n * @param uri MCP URI (must start with mcp://backlog/)\n * @returns Absolute file path\n * @throws Error if URI is invalid or contains path traversal\n */\n resolve(uri: string): string {\n if (!uri.startsWith('mcp://')) {\n throw new Error(`Not an MCP URI: ${uri}`);\n }\n\n // Check for path traversal BEFORE URL parsing (URL normalizes ..)\n if (uri.includes('..')) {\n throw new Error(`Path traversal not allowed: ${uri}`);\n }\n\n const url = new URL(uri);\n \n if (url.hostname !== 'backlog') {\n throw new Error(`Invalid hostname: ${url.hostname}. Expected 'backlog'`);\n }\n \n const path = url.pathname.substring(1); // Remove leading /\n \n return join(this.dataDir, path);\n }\n\n /**\n * Read resource content from MCP URI.\n * Parses frontmatter for markdown files and detects MIME type.\n * \n * @param uri MCP URI\n * @returns Resource content with frontmatter and MIME type\n * @throws Error if file not found\n */\n read(uri: string): ResourceContent {\n const filePath = this.resolve(uri);\n \n if (!existsSync(filePath)) {\n // Helpful error for common mistake: extension-less task URIs\n if (/^mcp:\\/\\/backlog\\/tasks\\/(TASK|EPIC)-\\d+$/.test(uri)) {\n throw new Error(\n `Task URIs must include .md extension. Did you mean: ${uri}.md?`\n );\n }\n throw new Error(`Resource not found: ${uri} (resolved to ${filePath})`);\n }\n \n const content = readFileSync(filePath, 'utf-8');\n const ext = filePath.split('.').pop()?.toLowerCase() || 'txt';\n const mimeType = this.getMimeType(ext);\n \n // Parse frontmatter for markdown files\n if (ext === 'md') {\n const parsed = matter(content);\n return {\n content: parsed.content,\n frontmatter: Object.keys(parsed.data).length > 0 ? parsed.data : undefined,\n mimeType,\n };\n }\n \n return {\n content,\n mimeType,\n };\n }\n\n /**\n * Write/modify resource content.\n * Applies operations like str_replace, append, insert, etc.\n * \n * @param uri MCP URI\n * @param operation Operation to apply\n * @returns Result with success status and message\n */\n write(uri: string, operation: Operation): WriteResourceResult {\n try {\n const filePath = this.resolve(uri);\n const canCreate = ['create', 'append', 'insert'].includes(operation.type);\n \n if (!existsSync(filePath)) {\n if (canCreate) {\n // Auto-create file and parent directories\n mkdirSync(dirname(filePath), { recursive: true });\n writeFileSync(filePath, '', 'utf-8');\n } else {\n // str_replace/delete need existing content\n return {\n success: false,\n message: 'File not found',\n error: `Resource not found: ${uri} (${operation.type} requires existing file)`,\n };\n }\n }\n\n const fileContent = readFileSync(filePath, 'utf-8');\n const newContent = applyOperation(fileContent, operation);\n writeFileSync(filePath, newContent, 'utf-8');\n\n return {\n success: true,\n message: `Successfully applied ${operation.type} to ${uri}`,\n };\n } catch (error) {\n return {\n success: false,\n message: 'Operation failed',\n error: error instanceof Error ? error.message : String(error),\n };\n }\n }\n\n /**\n * Convert file path to MCP URI.\n * Pure mapping: {dataDir}/path/file.md → mcp://backlog/path/file.md\n * \n * @param filePath Absolute file path\n * @returns MCP URI or null if file is outside data directory\n */\n toUri(filePath: string): string | null {\n if (!filePath.startsWith(this.dataDir)) {\n return null;\n }\n \n const relativePath = filePath.substring(this.dataDir.length + 1);\n return `mcp://backlog/${relativePath}`;\n }\n\n /**\n * Register MCP resource handler (catch-all pattern).\n */\n registerResource(server: McpServer) {\n const template = new ResourceTemplate(\n 'mcp://backlog/{+path}',\n { list: undefined }\n );\n \n server.registerResource(\n 'Data Directory Resource',\n template,\n { description: 'Any file in the backlog data directory' },\n async (uri) => {\n const resource = this.read(uri.toString());\n return { \n contents: [{ \n uri: uri.toString(), \n mimeType: resource.mimeType, \n text: resource.content \n }] \n };\n }\n );\n }\n\n /**\n * Register write_resource MCP tool.\n */\n registerWriteTool(server: McpServer) {\n server.registerTool(\n 'write_resource',\n {\n description: `A tool for creating and editing files on the MCP server\n * The \\`create\\` command will override the file at \\`uri\\` if it already exists as a file, and otherwise create a new file\n * The \\`append\\` command will add content to the end of a file, automatically adding a newline if the file doesn't end with one. Creates the file if it doesn't exist.\n Notes for using the \\`str_replace\\` command:\n * The \\`old_str\\` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n * If the \\`old_str\\` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in \\`old_str\\` to make it unique\n * The \\`new_str\\` parameter should contain the edited lines that should replace the \\`old_str\\``,\n inputSchema: z.object({\n uri: z.string().describe('MCP resource URI, e.g. mcp://backlog/path/to/file.md'),\n operation: z.preprocess(\n // Workaround: MCP clients stringify object params with $ref/oneOf schemas\n // https://github.com/anthropics/claude-code/issues/18260\n (val) => typeof val === 'string' ? JSON.parse(val) : val,\n z.discriminatedUnion('type', [\n z.object({\n type: z.literal('create'),\n file_text: z.string().describe('Content of the file to be created'),\n }),\n z.object({\n type: z.literal('str_replace'),\n old_str: z.string().describe('String in file to replace (must match exactly)'),\n new_str: z.string().describe('New string to replace old_str with'),\n }),\n z.object({\n type: z.literal('insert'),\n insert_line: z.number().describe('Line number after which new_str will be inserted'),\n new_str: z.string().describe('String to insert'),\n }),\n z.object({\n type: z.literal('append'),\n new_str: z.string().describe('Content to append to the file'),\n }),\n ])).describe('Operation to apply'),\n }),\n },\n async ({ uri, operation }) => {\n const result = this.write(uri, operation);\n return {\n content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],\n };\n }\n );\n }\n\n private getMimeType(ext: string): string {\n const mimeMap: Record<string, string> = {\n md: 'text/markdown',\n json: 'application/json',\n ts: 'text/typescript',\n js: 'application/javascript',\n txt: 'text/plain',\n };\n \n return mimeMap[ext] || 'text/plain';\n }\n}\n\n/**\n * Singleton instance for dependency injection.\n * Uses the configured backlog data directory.\n */\nexport const resourceManager = new ResourceManager(paths.backlogDataDir);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"manager.mjs","names":[],"sources":["../../src/resources/manager.ts"],"sourcesContent":["import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'node:fs';\nimport { join, dirname, relative, basename } from 'node:path';\nimport matter from 'gray-matter';\nimport { z } from 'zod';\nimport { paths } from '@/utils/paths.js';\nimport { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { Operation, WriteResourceResult } from './types.js';\nimport { applyOperation } from './operations.js';\nimport type { Resource } from '@/search/types.js';\n\nexport interface ResourceContent {\n content: string;\n frontmatter?: Record<string, any>;\n mimeType: string;\n}\n\n/**\n * Extract title from markdown content.\n * Returns first # heading or filename without extension.\n */\nfunction extractTitle(content: string, filename: string): string {\n const match = content.match(/^#\\s+(.+)$/m);\n return match?.[1]?.trim() || filename.replace(/\\.md$/, '');\n}\n\n/**\n * ResourceManager - Single point of responsibility for MCP resource operations.\n * \n * Pure catch-all design: mcp://backlog/{+path} → {dataDir}/{path}\n * No special cases, no magic behavior.\n */\nexport class ResourceManager {\n constructor(private readonly dataDir: string) {}\n\n /**\n * List all resources in the resources/ directory.\n * Returns Resource objects ready for search indexing.\n */\n list(): Resource[] {\n const resourcesDir = join(this.dataDir, 'resources');\n if (!existsSync(resourcesDir)) return [];\n\n const resources: Resource[] = [];\n this.scanDirectory(resourcesDir, resources);\n return resources;\n }\n\n private scanDirectory(dir: string, resources: Resource[]): void {\n const entries = readdirSync(dir, { withFileTypes: true });\n \n for (const entry of entries) {\n const fullPath = join(dir, entry.name);\n \n if (entry.isDirectory()) {\n this.scanDirectory(fullPath, resources);\n } else if (entry.isFile() && entry.name.endsWith('.md')) {\n try {\n const content = readFileSync(fullPath, 'utf-8');\n const relativePath = relative(this.dataDir, fullPath);\n const uri = `mcp://backlog/${relativePath}`;\n \n resources.push({\n id: uri,\n path: relativePath,\n title: extractTitle(content, entry.name),\n content,\n });\n } catch {\n // Skip files that can't be read\n }\n }\n }\n }\n\n /**\n * Resolve MCP URI to absolute file path.\n * Pure catch-all: mcp://backlog/path/file.md → {dataDir}/path/file.md\n * \n * @param uri MCP URI (must start with mcp://backlog/)\n * @returns Absolute file path\n * @throws Error if URI is invalid or contains path traversal\n */\n resolve(uri: string): string {\n if (!uri.startsWith('mcp://')) {\n throw new Error(`Not an MCP URI: ${uri}`);\n }\n\n // Check for path traversal BEFORE URL parsing (URL normalizes ..)\n if (uri.includes('..')) {\n throw new Error(`Path traversal not allowed: ${uri}`);\n }\n\n const url = new URL(uri);\n \n if (url.hostname !== 'backlog') {\n throw new Error(`Invalid hostname: ${url.hostname}. Expected 'backlog'`);\n }\n \n const path = url.pathname.substring(1); // Remove leading /\n \n return join(this.dataDir, path);\n }\n\n /**\n * Read resource content from MCP URI.\n * Parses frontmatter for markdown files and detects MIME type.\n * \n * @param uri MCP URI\n * @returns Resource content with frontmatter and MIME type\n * @throws Error if file not found\n */\n read(uri: string): ResourceContent {\n const filePath = this.resolve(uri);\n \n if (!existsSync(filePath)) {\n // Helpful error for common mistake: extension-less task URIs\n if (/^mcp:\\/\\/backlog\\/tasks\\/(TASK|EPIC)-\\d+$/.test(uri)) {\n throw new Error(\n `Task URIs must include .md extension. Did you mean: ${uri}.md?`\n );\n }\n throw new Error(`Resource not found: ${uri} (resolved to ${filePath})`);\n }\n \n const content = readFileSync(filePath, 'utf-8');\n const ext = filePath.split('.').pop()?.toLowerCase() || 'txt';\n const mimeType = this.getMimeType(ext);\n \n // Parse frontmatter for markdown files\n if (ext === 'md') {\n const parsed = matter(content);\n return {\n content: parsed.content,\n frontmatter: Object.keys(parsed.data).length > 0 ? parsed.data : undefined,\n mimeType,\n };\n }\n \n return {\n content,\n mimeType,\n };\n }\n\n /**\n * Write/modify resource content.\n * Applies operations like str_replace, append, insert, etc.\n * \n * @param uri MCP URI\n * @param operation Operation to apply\n * @returns Result with success status and message\n */\n write(uri: string, operation: Operation): WriteResourceResult {\n try {\n const filePath = this.resolve(uri);\n const canCreate = ['create', 'append', 'insert'].includes(operation.type);\n \n if (!existsSync(filePath)) {\n if (canCreate) {\n // Auto-create file and parent directories\n mkdirSync(dirname(filePath), { recursive: true });\n writeFileSync(filePath, '', 'utf-8');\n } else {\n // str_replace/delete need existing content\n return {\n success: false,\n message: 'File not found',\n error: `Resource not found: ${uri} (${operation.type} requires existing file)`,\n };\n }\n }\n\n const fileContent = readFileSync(filePath, 'utf-8');\n const newContent = applyOperation(fileContent, operation);\n writeFileSync(filePath, newContent, 'utf-8');\n\n return {\n success: true,\n message: `Successfully applied ${operation.type} to ${uri}`,\n };\n } catch (error) {\n return {\n success: false,\n message: 'Operation failed',\n error: error instanceof Error ? error.message : String(error),\n };\n }\n }\n\n /**\n * Convert file path to MCP URI.\n * Pure mapping: {dataDir}/path/file.md → mcp://backlog/path/file.md\n * \n * @param filePath Absolute file path\n * @returns MCP URI or null if file is outside data directory\n */\n toUri(filePath: string): string | null {\n if (!filePath.startsWith(this.dataDir)) {\n return null;\n }\n \n const relativePath = filePath.substring(this.dataDir.length + 1);\n return `mcp://backlog/${relativePath}`;\n }\n\n /**\n * Register MCP resource handler (catch-all pattern).\n */\n registerResource(server: McpServer) {\n const template = new ResourceTemplate(\n 'mcp://backlog/{+path}',\n { list: undefined }\n );\n \n server.registerResource(\n 'Data Directory Resource',\n template,\n { description: 'Any file in the backlog data directory' },\n async (uri) => {\n const resource = this.read(uri.toString());\n return { \n contents: [{ \n uri: uri.toString(), \n mimeType: resource.mimeType, \n text: resource.content \n }] \n };\n }\n );\n }\n\n /**\n * Register write_resource MCP tool.\n */\n registerWriteTool(server: McpServer) {\n server.registerTool(\n 'write_resource',\n {\n description: `A tool for creating and editing files on the MCP server\n * The \\`create\\` command will override the file at \\`uri\\` if it already exists as a file, and otherwise create a new file\n * The \\`append\\` command will add content to the end of a file, automatically adding a newline if the file doesn't end with one. Creates the file if it doesn't exist.\n Notes for using the \\`str_replace\\` command:\n * The \\`old_str\\` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n * If the \\`old_str\\` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in \\`old_str\\` to make it unique\n * The \\`new_str\\` parameter should contain the edited lines that should replace the \\`old_str\\``,\n inputSchema: z.object({\n uri: z.string().describe('MCP resource URI, e.g. mcp://backlog/path/to/file.md'),\n operation: z.preprocess(\n // Workaround: MCP clients stringify object params with $ref/oneOf schemas\n // https://github.com/anthropics/claude-code/issues/18260\n (val) => typeof val === 'string' ? JSON.parse(val) : val,\n z.discriminatedUnion('type', [\n z.object({\n type: z.literal('create'),\n file_text: z.string().describe('Content of the file to be created'),\n }),\n z.object({\n type: z.literal('str_replace'),\n old_str: z.string().describe('String in file to replace (must match exactly)'),\n new_str: z.string().describe('New string to replace old_str with'),\n }),\n z.object({\n type: z.literal('insert'),\n insert_line: z.number().describe('Line number after which new_str will be inserted'),\n new_str: z.string().describe('String to insert'),\n }),\n z.object({\n type: z.literal('append'),\n new_str: z.string().describe('Content to append to the file'),\n }),\n ])).describe('Operation to apply'),\n }),\n },\n async ({ uri, operation }) => {\n const result = this.write(uri, operation);\n return {\n content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],\n };\n }\n );\n }\n\n private getMimeType(ext: string): string {\n const mimeMap: Record<string, string> = {\n md: 'text/markdown',\n json: 'application/json',\n ts: 'text/typescript',\n js: 'application/javascript',\n txt: 'text/plain',\n };\n \n return mimeMap[ext] || 'text/plain';\n }\n}\n\n/**\n * Singleton instance for dependency injection.\n * Uses the configured backlog data directory.\n */\nexport const resourceManager = new ResourceManager(paths.backlogDataDir);\n"],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/search/embedding-service.d.ts
|
|
2
|
+
declare const EMBEDDING_DIMENSIONS = 384;
|
|
3
|
+
/**
|
|
4
|
+
* Local embedding service using transformers.js.
|
|
5
|
+
* Generates 384-dimensional vectors for semantic search.
|
|
6
|
+
*/
|
|
7
|
+
declare class EmbeddingService {
|
|
8
|
+
private embedder;
|
|
9
|
+
private initPromise;
|
|
10
|
+
/**
|
|
11
|
+
* Initialize the embedding model (lazy, called on first use).
|
|
12
|
+
* Downloads model on first run (~23MB, cached in ~/.cache/huggingface).
|
|
13
|
+
*/
|
|
14
|
+
init(): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Generate embedding vector for text.
|
|
17
|
+
* @returns 384-dimensional normalized vector
|
|
18
|
+
*/
|
|
19
|
+
embed(text: string): Promise<number[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Check if the service is ready (model loaded).
|
|
22
|
+
*/
|
|
23
|
+
isReady(): boolean;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { EMBEDDING_DIMENSIONS, EmbeddingService };
|
|
27
|
+
//# sourceMappingURL=embedding-service.d.mts.map
|