backlog-mcp 0.25.1 → 0.26.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 +3 -3
- package/dist/resources/index.d.mts +2 -2
- package/dist/resources/index.mjs +2 -2
- package/dist/resources/manager.d.mts +71 -0
- package/dist/resources/manager.mjs +186 -0
- package/dist/resources/manager.mjs.map +1 -0
- package/dist/server/mcp-handler.mjs +3 -2
- package/dist/server/mcp-handler.mjs.map +1 -1
- package/dist/server/viewer-routes.mjs +4 -5
- package/dist/server/viewer-routes.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/resources/data-dir.d.mts +0 -23
- package/dist/resources/data-dir.mjs +0 -36
- package/dist/resources/data-dir.mjs.map +0 -1
- package/dist/resources/index-resources.d.mts +0 -19
- package/dist/resources/index-resources.mjs +0 -22
- package/dist/resources/index-resources.mjs.map +0 -1
- package/dist/resources/resource-file.d.mts +0 -21
- package/dist/resources/resource-file.mjs +0 -38
- package/dist/resources/resource-file.mjs.map +0 -1
- package/dist/resources/resource-reader.d.mts +0 -10
- package/dist/resources/resource-reader.mjs +0 -34
- package/dist/resources/resource-reader.mjs.map +0 -1
- package/dist/resources/task-attached.d.mts +0 -21
- package/dist/resources/task-attached.mjs +0 -36
- package/dist/resources/task-attached.mjs.map +0 -1
- package/dist/resources/task-by-id.d.mts +0 -7
- package/dist/resources/task-by-id.mjs +0 -23
- package/dist/resources/task-by-id.mjs.map +0 -1
- package/dist/resources/tasks.d.mts +0 -7
- package/dist/resources/tasks.mjs +0 -20
- package/dist/resources/tasks.mjs.map +0 -1
- package/dist/resources/uri.d.mts +0 -11
- package/dist/resources/uri.mjs +0 -22
- package/dist/resources/uri.mjs.map +0 -1
- package/dist/resources/write.d.mts +0 -11
- package/dist/resources/write.mjs +0 -63
- package/dist/resources/write.mjs.map +0 -1
- package/dist/utils/uri-resolver.d.mts +0 -7
- package/dist/utils/uri-resolver.mjs +0 -42
- package/dist/utils/uri-resolver.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -85,12 +85,12 @@ backlog_delete id="TASK-0001" # Permanently delete
|
|
|
85
85
|
Access tasks and resources via MCP resource URIs:
|
|
86
86
|
|
|
87
87
|
```
|
|
88
|
-
mcp://backlog/tasks
|
|
89
|
-
mcp://backlog/tasks/TASK-0001 # Single task (JSON)
|
|
88
|
+
mcp://backlog/tasks/TASK-0001.md # Task markdown file
|
|
90
89
|
mcp://backlog/resources/TASK-0001/adr.md # Task-attached resource
|
|
90
|
+
mcp://backlog/resources/investigation.md # Standalone resource
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
Modify resources via `write_resource` tool with operations like `str_replace`, `append`, `insert`.
|
|
94
94
|
|
|
95
95
|
## Installation
|
|
96
96
|
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { Operation, WriteResourceResult } from "./types.mjs";
|
|
2
|
-
import {
|
|
3
|
-
export { type Operation, type WriteResourceResult,
|
|
2
|
+
import { ResourceContent, ResourceManager, resourceManager } from "./manager.mjs";
|
|
3
|
+
export { type Operation, type ResourceContent, ResourceManager, type WriteResourceResult, resourceManager };
|
package/dist/resources/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ResourceManager, resourceManager } from "./manager.mjs";
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { ResourceManager, resourceManager };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Operation, WriteResourceResult } from "./types.mjs";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
|
|
4
|
+
//#region src/resources/manager.d.ts
|
|
5
|
+
interface ResourceContent {
|
|
6
|
+
content: string;
|
|
7
|
+
frontmatter?: Record<string, any>;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* ResourceManager - Single point of responsibility for MCP resource operations.
|
|
12
|
+
*
|
|
13
|
+
* Pure catch-all design: mcp://backlog/{+path} → {dataDir}/{path}
|
|
14
|
+
* No special cases, no magic behavior.
|
|
15
|
+
*/
|
|
16
|
+
declare class ResourceManager {
|
|
17
|
+
private readonly dataDir;
|
|
18
|
+
constructor(dataDir: string);
|
|
19
|
+
/**
|
|
20
|
+
* Resolve MCP URI to absolute file path.
|
|
21
|
+
* Pure catch-all: mcp://backlog/path/file.md → {dataDir}/path/file.md
|
|
22
|
+
*
|
|
23
|
+
* @param uri MCP URI (must start with mcp://backlog/)
|
|
24
|
+
* @returns Absolute file path
|
|
25
|
+
* @throws Error if URI is invalid or contains path traversal
|
|
26
|
+
*/
|
|
27
|
+
resolve(uri: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Read resource content from MCP URI.
|
|
30
|
+
* Parses frontmatter for markdown files and detects MIME type.
|
|
31
|
+
*
|
|
32
|
+
* @param uri MCP URI
|
|
33
|
+
* @returns Resource content with frontmatter and MIME type
|
|
34
|
+
* @throws Error if file not found
|
|
35
|
+
*/
|
|
36
|
+
read(uri: string): ResourceContent;
|
|
37
|
+
/**
|
|
38
|
+
* Write/modify resource content.
|
|
39
|
+
* Applies operations like str_replace, append, insert, etc.
|
|
40
|
+
*
|
|
41
|
+
* @param uri MCP URI
|
|
42
|
+
* @param operation Operation to apply
|
|
43
|
+
* @returns Result with success status and message
|
|
44
|
+
*/
|
|
45
|
+
write(uri: string, operation: Operation): WriteResourceResult;
|
|
46
|
+
/**
|
|
47
|
+
* Convert file path to MCP URI.
|
|
48
|
+
* Pure mapping: {dataDir}/path/file.md → mcp://backlog/path/file.md
|
|
49
|
+
*
|
|
50
|
+
* @param filePath Absolute file path
|
|
51
|
+
* @returns MCP URI or null if file is outside data directory
|
|
52
|
+
*/
|
|
53
|
+
toUri(filePath: string): string | null;
|
|
54
|
+
/**
|
|
55
|
+
* Register MCP resource handler (catch-all pattern).
|
|
56
|
+
*/
|
|
57
|
+
registerResource(server: McpServer): void;
|
|
58
|
+
/**
|
|
59
|
+
* Register write_resource MCP tool.
|
|
60
|
+
*/
|
|
61
|
+
registerWriteTool(server: McpServer): void;
|
|
62
|
+
private getMimeType;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Singleton instance for dependency injection.
|
|
66
|
+
* Uses the configured backlog data directory.
|
|
67
|
+
*/
|
|
68
|
+
declare const resourceManager: ResourceManager;
|
|
69
|
+
//#endregion
|
|
70
|
+
export { ResourceContent, ResourceManager, resourceManager };
|
|
71
|
+
//# sourceMappingURL=manager.d.mts.map
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { paths } from "../utils/paths.mjs";
|
|
2
|
+
import { applyOperation } from "./operations.mjs";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import matter from "gray-matter";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
|
|
9
|
+
//#region src/resources/manager.ts
|
|
10
|
+
/**
|
|
11
|
+
* ResourceManager - Single point of responsibility for MCP resource operations.
|
|
12
|
+
*
|
|
13
|
+
* Pure catch-all design: mcp://backlog/{+path} → {dataDir}/{path}
|
|
14
|
+
* No special cases, no magic behavior.
|
|
15
|
+
*/
|
|
16
|
+
var ResourceManager = class {
|
|
17
|
+
constructor(dataDir) {
|
|
18
|
+
this.dataDir = dataDir;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve MCP URI to absolute file path.
|
|
22
|
+
* Pure catch-all: mcp://backlog/path/file.md → {dataDir}/path/file.md
|
|
23
|
+
*
|
|
24
|
+
* @param uri MCP URI (must start with mcp://backlog/)
|
|
25
|
+
* @returns Absolute file path
|
|
26
|
+
* @throws Error if URI is invalid or contains path traversal
|
|
27
|
+
*/
|
|
28
|
+
resolve(uri) {
|
|
29
|
+
if (!uri.startsWith("mcp://")) throw new Error(`Not an MCP URI: ${uri}`);
|
|
30
|
+
if (uri.includes("..")) throw new Error(`Path traversal not allowed: ${uri}`);
|
|
31
|
+
const url = new URL(uri);
|
|
32
|
+
if (url.hostname !== "backlog") throw new Error(`Invalid hostname: ${url.hostname}. Expected 'backlog'`);
|
|
33
|
+
const path = url.pathname.substring(1);
|
|
34
|
+
return join(this.dataDir, path);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Read resource content from MCP URI.
|
|
38
|
+
* Parses frontmatter for markdown files and detects MIME type.
|
|
39
|
+
*
|
|
40
|
+
* @param uri MCP URI
|
|
41
|
+
* @returns Resource content with frontmatter and MIME type
|
|
42
|
+
* @throws Error if file not found
|
|
43
|
+
*/
|
|
44
|
+
read(uri) {
|
|
45
|
+
const filePath = this.resolve(uri);
|
|
46
|
+
if (!existsSync(filePath)) {
|
|
47
|
+
if (/^mcp:\/\/backlog\/tasks\/(TASK|EPIC)-\d+$/.test(uri)) throw new Error(`Task URIs must include .md extension. Did you mean: ${uri}.md?`);
|
|
48
|
+
throw new Error(`Resource not found: ${uri} (resolved to ${filePath})`);
|
|
49
|
+
}
|
|
50
|
+
const content = readFileSync(filePath, "utf-8");
|
|
51
|
+
const ext = filePath.split(".").pop()?.toLowerCase() || "txt";
|
|
52
|
+
const mimeType = this.getMimeType(ext);
|
|
53
|
+
if (ext === "md") {
|
|
54
|
+
const parsed = matter(content);
|
|
55
|
+
return {
|
|
56
|
+
content: parsed.content,
|
|
57
|
+
frontmatter: Object.keys(parsed.data).length > 0 ? parsed.data : void 0,
|
|
58
|
+
mimeType
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
content,
|
|
63
|
+
mimeType
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Write/modify resource content.
|
|
68
|
+
* Applies operations like str_replace, append, insert, etc.
|
|
69
|
+
*
|
|
70
|
+
* @param uri MCP URI
|
|
71
|
+
* @param operation Operation to apply
|
|
72
|
+
* @returns Result with success status and message
|
|
73
|
+
*/
|
|
74
|
+
write(uri, operation) {
|
|
75
|
+
try {
|
|
76
|
+
const filePath = this.resolve(uri);
|
|
77
|
+
if (!existsSync(filePath)) {
|
|
78
|
+
if (/^mcp:\/\/backlog\/tasks\/(TASK|EPIC)-\d+$/.test(uri)) return {
|
|
79
|
+
success: false,
|
|
80
|
+
message: "Task URIs must include .md extension",
|
|
81
|
+
error: `Did you mean: ${uri}.md?`
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
success: false,
|
|
85
|
+
message: "File not found",
|
|
86
|
+
error: `Resource not found: ${uri}`
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
writeFileSync(filePath, applyOperation(readFileSync(filePath, "utf-8"), operation), "utf-8");
|
|
90
|
+
return {
|
|
91
|
+
success: true,
|
|
92
|
+
message: `Successfully applied ${operation.type} to ${uri}`
|
|
93
|
+
};
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
message: "Operation failed",
|
|
98
|
+
error: error instanceof Error ? error.message : String(error)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Convert file path to MCP URI.
|
|
104
|
+
* Pure mapping: {dataDir}/path/file.md → mcp://backlog/path/file.md
|
|
105
|
+
*
|
|
106
|
+
* @param filePath Absolute file path
|
|
107
|
+
* @returns MCP URI or null if file is outside data directory
|
|
108
|
+
*/
|
|
109
|
+
toUri(filePath) {
|
|
110
|
+
if (!filePath.startsWith(this.dataDir)) return null;
|
|
111
|
+
return `mcp://backlog/${filePath.substring(this.dataDir.length + 1)}`;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Register MCP resource handler (catch-all pattern).
|
|
115
|
+
*/
|
|
116
|
+
registerResource(server) {
|
|
117
|
+
const template = new ResourceTemplate("mcp://backlog/{+path}", { list: void 0 });
|
|
118
|
+
server.registerResource("Data Directory Resource", template, { description: "Any file in the backlog data directory" }, async (uri) => {
|
|
119
|
+
const resource = this.read(uri.toString());
|
|
120
|
+
return { contents: [{
|
|
121
|
+
uri: uri.toString(),
|
|
122
|
+
mimeType: resource.mimeType,
|
|
123
|
+
text: resource.content
|
|
124
|
+
}] };
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Register write_resource MCP tool.
|
|
129
|
+
*/
|
|
130
|
+
registerWriteTool(server) {
|
|
131
|
+
server.registerTool("write_resource", {
|
|
132
|
+
description: "Write/modify resource content with operations like str_replace, append, insert",
|
|
133
|
+
inputSchema: z.object({
|
|
134
|
+
uri: z.string().describe("MCP URI (mcp://backlog/path/file.md)"),
|
|
135
|
+
operation: z.discriminatedUnion("type", [
|
|
136
|
+
z.object({
|
|
137
|
+
type: z.literal("str_replace"),
|
|
138
|
+
old_str: z.string().describe("String to find and replace"),
|
|
139
|
+
new_str: z.string().describe("Replacement string")
|
|
140
|
+
}),
|
|
141
|
+
z.object({
|
|
142
|
+
type: z.literal("append"),
|
|
143
|
+
content: z.string().describe("Content to append")
|
|
144
|
+
}),
|
|
145
|
+
z.object({
|
|
146
|
+
type: z.literal("prepend"),
|
|
147
|
+
content: z.string().describe("Content to prepend")
|
|
148
|
+
}),
|
|
149
|
+
z.object({
|
|
150
|
+
type: z.literal("insert"),
|
|
151
|
+
line: z.number().describe("Line number to insert at (0-based)"),
|
|
152
|
+
content: z.string().describe("Content to insert")
|
|
153
|
+
}),
|
|
154
|
+
z.object({
|
|
155
|
+
type: z.literal("delete"),
|
|
156
|
+
content: z.string().describe("Content to delete")
|
|
157
|
+
})
|
|
158
|
+
]).describe("Operation to apply")
|
|
159
|
+
})
|
|
160
|
+
}, async ({ uri, operation }) => {
|
|
161
|
+
const result = this.write(uri, operation);
|
|
162
|
+
return { content: [{
|
|
163
|
+
type: "text",
|
|
164
|
+
text: JSON.stringify(result, null, 2)
|
|
165
|
+
}] };
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
getMimeType(ext) {
|
|
169
|
+
return {
|
|
170
|
+
md: "text/markdown",
|
|
171
|
+
json: "application/json",
|
|
172
|
+
ts: "text/typescript",
|
|
173
|
+
js: "application/javascript",
|
|
174
|
+
txt: "text/plain"
|
|
175
|
+
}[ext] || "text/plain";
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Singleton instance for dependency injection.
|
|
180
|
+
* Uses the configured backlog data directory.
|
|
181
|
+
*/
|
|
182
|
+
const resourceManager = new ResourceManager(paths.backlogDataDir);
|
|
183
|
+
|
|
184
|
+
//#endregion
|
|
185
|
+
export { ResourceManager, resourceManager };
|
|
186
|
+
//# sourceMappingURL=manager.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.mjs","names":[],"sources":["../../src/resources/manager.ts"],"sourcesContent":["import { readFileSync, existsSync, writeFileSync } from 'node:fs';\nimport { join } 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 \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 return {\n success: false,\n message: 'Task URIs must include .md extension',\n error: `Did you mean: ${uri}.md?`,\n };\n }\n return {\n success: false,\n message: 'File not found',\n error: `Resource not found: ${uri}`,\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: 'Write/modify resource content with operations like str_replace, append, insert',\n inputSchema: z.object({\n uri: z.string().describe('MCP URI (mcp://backlog/path/file.md)'),\n operation: z.discriminatedUnion('type', [\n z.object({\n type: z.literal('str_replace'),\n old_str: z.string().describe('String to find and replace'),\n new_str: z.string().describe('Replacement string'),\n }),\n z.object({\n type: z.literal('append'),\n content: z.string().describe('Content to append'),\n }),\n z.object({\n type: z.literal('prepend'),\n content: z.string().describe('Content to prepend'),\n }),\n z.object({\n type: z.literal('insert'),\n line: z.number().describe('Line number to insert at (0-based)'),\n content: z.string().describe('Content to insert'),\n }),\n z.object({\n type: z.literal('delete'),\n content: z.string().describe('Content to delete'),\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,6 +1,6 @@
|
|
|
1
1
|
import { paths } from "../utils/paths.mjs";
|
|
2
|
+
import { resourceManager } from "../resources/manager.mjs";
|
|
2
3
|
import { registerTools } from "../tools/index.mjs";
|
|
3
|
-
import { registerResources } from "../resources/index-resources.mjs";
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
6
|
|
|
@@ -12,7 +12,8 @@ function registerMcpHandler(app) {
|
|
|
12
12
|
version: paths.getVersion()
|
|
13
13
|
});
|
|
14
14
|
registerTools(server);
|
|
15
|
-
|
|
15
|
+
resourceManager.registerResource(server);
|
|
16
|
+
resourceManager.registerWriteTool(server);
|
|
16
17
|
const transport = new StreamableHTTPServerTransport({
|
|
17
18
|
sessionIdGenerator: void 0,
|
|
18
19
|
enableJsonResponse: true
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-handler.mjs","names":[],"sources":["../../src/server/mcp-handler.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport { registerTools } from '@/tools/index.js';\nimport {
|
|
1
|
+
{"version":3,"file":"mcp-handler.mjs","names":[],"sources":["../../src/server/mcp-handler.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport { registerTools } from '@/tools/index.js';\nimport { resourceManager } from '@/resources/manager.js';\nimport { paths } from '@/utils/paths.js';\n\nexport function registerMcpHandler(app: FastifyInstance) {\n app.all('/mcp', async (request, reply) => {\n const server = new McpServer({ \n name: paths.packageJson.name, \n version: paths.getVersion() \n });\n \n registerTools(server);\n resourceManager.registerResource(server);\n resourceManager.registerWriteTool(server);\n \n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true\n });\n \n await server.connect(transport);\n \n reply.hijack();\n \n reply.raw.on('close', () => {\n transport.close().catch(() => {});\n server.close().catch(() => {});\n });\n \n await transport.handleRequest(request.raw, reply.raw, request.body);\n });\n}\n"],"mappings":";;;;;;;AAOA,SAAgB,mBAAmB,KAAsB;AACvD,KAAI,IAAI,QAAQ,OAAO,SAAS,UAAU;EACxC,MAAM,SAAS,IAAI,UAAU;GAC3B,MAAM,MAAM,YAAY;GACxB,SAAS,MAAM,YAAY;GAC5B,CAAC;AAEF,gBAAc,OAAO;AACrB,kBAAgB,iBAAiB,OAAO;AACxC,kBAAgB,kBAAkB,OAAO;EAEzC,MAAM,YAAY,IAAI,8BAA8B;GAClD,oBAAoB;GACpB,oBAAoB;GACrB,CAAC;AAEF,QAAM,OAAO,QAAQ,UAAU;AAE/B,QAAM,QAAQ;AAEd,QAAM,IAAI,GAAG,eAAe;AAC1B,aAAU,OAAO,CAAC,YAAY,GAAG;AACjC,UAAO,OAAO,CAAC,YAAY,GAAG;IAC9B;AAEF,QAAM,UAAU,cAAc,QAAQ,KAAK,MAAM,KAAK,QAAQ,KAAK;GACnE"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { paths } from "../utils/paths.mjs";
|
|
2
2
|
import { storage } from "../storage/backlog.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { readMcpResource } from "../resources/resource-reader.mjs";
|
|
3
|
+
import { resourceManager } from "../resources/manager.mjs";
|
|
5
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
5
|
import matter from "gray-matter";
|
|
7
6
|
import fastifyStatic from "@fastify/static";
|
|
@@ -86,7 +85,7 @@ function registerViewerRoutes(app) {
|
|
|
86
85
|
type: mimeMap[ext] || "text/plain",
|
|
87
86
|
path: filePath,
|
|
88
87
|
fileUri: `file://${filePath}`,
|
|
89
|
-
mcpUri:
|
|
88
|
+
mcpUri: resourceManager.toUri(filePath),
|
|
90
89
|
ext
|
|
91
90
|
};
|
|
92
91
|
} catch (error) {
|
|
@@ -100,8 +99,8 @@ function registerViewerRoutes(app) {
|
|
|
100
99
|
const { uri } = request.query;
|
|
101
100
|
if (!uri || !uri.startsWith("mcp://backlog/")) return reply.code(400).send({ error: "Invalid MCP URI" });
|
|
102
101
|
try {
|
|
103
|
-
const resource =
|
|
104
|
-
const filePath =
|
|
102
|
+
const resource = resourceManager.read(uri);
|
|
103
|
+
const filePath = resourceManager.resolve(uri);
|
|
105
104
|
const ext = filePath.split(".").pop()?.toLowerCase() || "txt";
|
|
106
105
|
return {
|
|
107
106
|
content: resource.content,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"viewer-routes.mjs","names":[],"sources":["../../src/server/viewer-routes.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport fastifyStatic from '@fastify/static';\nimport { exec } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport matter from 'gray-matter';\nimport { storage } from '../storage/backlog.js';\nimport {
|
|
1
|
+
{"version":3,"file":"viewer-routes.mjs","names":[],"sources":["../../src/server/viewer-routes.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport fastifyStatic from '@fastify/static';\nimport { exec } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport matter from 'gray-matter';\nimport { storage } from '../storage/backlog.js';\nimport { resourceManager } from '../resources/manager.js';\nimport { paths } from '../utils/paths.js';\n\nexport function registerViewerRoutes(app: FastifyInstance) {\n // Static files - serve from dist/viewer (built assets)\n app.register(fastifyStatic, {\n root: paths.viewerDist,\n prefix: '/',\n });\n\n // List tasks\n app.get('/tasks', async (request) => {\n const { filter, limit } = request.query as { filter?: string; limit?: string };\n \n const statusMap: Record<string, any> = {\n active: { status: ['open', 'in_progress', 'blocked'] },\n done: { status: ['done'] },\n cancelled: { status: ['cancelled'] },\n all: {},\n };\n \n const filterConfig = statusMap[filter || 'active'] || statusMap.active;\n const tasks = storage.list({ ...filterConfig, limit: limit ? parseInt(limit) : 100 });\n \n return tasks;\n });\n\n // Get single task\n app.get('/tasks/:id', async (request, reply) => {\n const { id } = request.params as { id: string };\n const task = storage.get(id);\n \n if (!task) {\n return reply.code(404).send({ error: 'Task not found' });\n }\n \n return task;\n });\n\n // System status\n app.get('/api/status', async () => {\n const tasks = storage.list({ limit: 10000 });\n const address = app.server.address();\n const port = typeof address === 'object' && address ? address.port : 3030;\n \n return {\n version: paths.getVersion(),\n port,\n dataDir: paths.backlogDataDir,\n taskCount: tasks.length,\n uptime: Math.floor(process.uptime())\n };\n });\n\n // Open task in editor\n app.get('/open/:id', async (request, reply) => {\n const { id } = request.params as { id: string };\n const filePath = storage.getFilePath(id);\n \n if (!filePath) {\n return reply.code(404).send({ error: 'Task not found' });\n }\n \n exec(`open \"${filePath}\"`);\n return { status: 'Opening...' };\n });\n\n // Resource proxy\n app.get('/resource', async (request, reply) => {\n const { path: filePath } = request.query as { path?: string };\n \n if (!filePath) {\n return reply.code(400).send({ error: 'Missing path parameter' });\n }\n \n if (!existsSync(filePath)) {\n return reply.code(404).send({ error: 'File not found', path: filePath });\n }\n \n try {\n const content = readFileSync(filePath, 'utf-8');\n const ext = filePath.split('.').pop()?.toLowerCase() || 'txt';\n const mimeMap: Record<string, string> = {\n md: 'text/markdown',\n ts: 'text/typescript',\n js: 'text/javascript',\n json: 'application/json',\n txt: 'text/plain',\n };\n \n let frontmatter = {};\n let bodyContent = content;\n \n // Parse frontmatter for markdown files\n if (ext === 'md') {\n const parsed = matter(content);\n frontmatter = parsed.data;\n bodyContent = parsed.content;\n }\n \n return {\n content: bodyContent,\n frontmatter,\n type: mimeMap[ext] || 'text/plain',\n path: filePath,\n fileUri: `file://${filePath}`,\n mcpUri: resourceManager.toUri(filePath),\n ext\n };\n } catch (error: any) {\n return reply.code(500).send({ error: 'Failed to read file', message: error.message });\n }\n });\n\n // MCP resource proxy\n app.get('/mcp/resource', async (request, reply) => {\n const { uri } = request.query as { uri?: string };\n \n if (!uri || !uri.startsWith('mcp://backlog/')) {\n return reply.code(400).send({ error: 'Invalid MCP URI' });\n }\n \n try {\n const resource = resourceManager.read(uri);\n const filePath = resourceManager.resolve(uri);\n const ext = filePath.split('.').pop()?.toLowerCase() || 'txt';\n \n return {\n content: resource.content,\n frontmatter: resource.frontmatter || {},\n type: resource.mimeType,\n path: filePath,\n fileUri: `file://${filePath}`,\n mcpUri: uri,\n ext\n };\n } catch (error: any) {\n return reply.code(404).send({ error: 'Resource not found', uri, message: error.message });\n }\n });\n\n // Open resource in viewer\n app.get('/open', async (request, reply) => {\n const { uri } = request.query as { uri?: string };\n \n if (!uri) {\n return reply.code(400).send({ error: 'Missing uri parameter' });\n }\n \n return reply.redirect(`/?resource=${encodeURIComponent(uri)}`);\n });\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
|
|
3
|
-
//#region src/resources/data-dir.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* Catch-all handler for any path in the backlog data directory.
|
|
6
|
-
*
|
|
7
|
-
* URI Template (RFC 6570): mcp://backlog/{+path}
|
|
8
|
-
* - {+path}: Greedy match - captures everything including slashes
|
|
9
|
-
*
|
|
10
|
-
* Examples:
|
|
11
|
-
* ✅ mcp://backlog/backlog-mcp-engineer/system-config-visibility-2026-01-26/artifact.md
|
|
12
|
-
* ✅ mcp://backlog/artifacts/some-file.md
|
|
13
|
-
* ✅ mcp://backlog/any/nested/path/file.md
|
|
14
|
-
*
|
|
15
|
-
* Storage location: {BACKLOG_DATA_DIR}/{path}
|
|
16
|
-
*
|
|
17
|
-
* Note: This is the lowest priority handler (registered last).
|
|
18
|
-
* More specific handlers (tasks, task-attached resources) take precedence.
|
|
19
|
-
*/
|
|
20
|
-
declare function registerDataDirResource(server: McpServer): void;
|
|
21
|
-
//#endregion
|
|
22
|
-
export { registerDataDirResource };
|
|
23
|
-
//# sourceMappingURL=data-dir.d.mts.map
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { readMcpResource } from "./resource-reader.mjs";
|
|
2
|
-
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
|
|
4
|
-
//#region src/resources/data-dir.ts
|
|
5
|
-
/**
|
|
6
|
-
* Catch-all handler for any path in the backlog data directory.
|
|
7
|
-
*
|
|
8
|
-
* URI Template (RFC 6570): mcp://backlog/{+path}
|
|
9
|
-
* - {+path}: Greedy match - captures everything including slashes
|
|
10
|
-
*
|
|
11
|
-
* Examples:
|
|
12
|
-
* ✅ mcp://backlog/backlog-mcp-engineer/system-config-visibility-2026-01-26/artifact.md
|
|
13
|
-
* ✅ mcp://backlog/artifacts/some-file.md
|
|
14
|
-
* ✅ mcp://backlog/any/nested/path/file.md
|
|
15
|
-
*
|
|
16
|
-
* Storage location: {BACKLOG_DATA_DIR}/{path}
|
|
17
|
-
*
|
|
18
|
-
* Note: This is the lowest priority handler (registered last).
|
|
19
|
-
* More specific handlers (tasks, task-attached resources) take precedence.
|
|
20
|
-
*/
|
|
21
|
-
function registerDataDirResource(server) {
|
|
22
|
-
const template = new ResourceTemplate("mcp://backlog/{+path}", { list: void 0 });
|
|
23
|
-
server.registerResource("Data Directory Resource", template, { description: "Any file in the backlog data directory" }, async (uri, variables) => {
|
|
24
|
-
String(variables.path);
|
|
25
|
-
const resource = await readMcpResource(uri.toString());
|
|
26
|
-
return { contents: [{
|
|
27
|
-
uri: uri.toString(),
|
|
28
|
-
mimeType: resource.mimeType,
|
|
29
|
-
text: resource.content
|
|
30
|
-
}] };
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
//#endregion
|
|
35
|
-
export { registerDataDirResource };
|
|
36
|
-
//# sourceMappingURL=data-dir.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"data-dir.mjs","names":[],"sources":["../../src/resources/data-dir.ts"],"sourcesContent":["import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { readMcpResource } from './resource-reader.js';\n\n/**\n * Catch-all handler for any path in the backlog data directory.\n * \n * URI Template (RFC 6570): mcp://backlog/{+path}\n * - {+path}: Greedy match - captures everything including slashes\n * \n * Examples:\n * ✅ mcp://backlog/backlog-mcp-engineer/system-config-visibility-2026-01-26/artifact.md\n * ✅ mcp://backlog/artifacts/some-file.md\n * ✅ mcp://backlog/any/nested/path/file.md\n * \n * Storage location: {BACKLOG_DATA_DIR}/{path}\n * \n * Note: This is the lowest priority handler (registered last).\n * More specific handlers (tasks, task-attached resources) take precedence.\n */\nexport function registerDataDirResource(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, variables) => {\n const path = String(variables.path);\n \n const resource = await readMcpResource(uri.toString());\n return { contents: [{ uri: uri.toString(), mimeType: resource.mimeType, text: resource.content }] };\n }\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,wBAAwB,QAAmB;CACzD,MAAM,WAAW,IAAI,iBACnB,yBACA,EAAE,MAAM,QAAW,CACpB;AAED,QAAO,iBACL,2BACA,UACA,EAAE,aAAa,0CAA0C,EACzD,OAAO,KAAK,cAAc;AACX,SAAO,UAAU,KAAK;EAEnC,MAAM,WAAW,MAAM,gBAAgB,IAAI,UAAU,CAAC;AACtD,SAAO,EAAE,UAAU,CAAC;GAAE,KAAK,IAAI,UAAU;GAAE,UAAU,SAAS;GAAU,MAAM,SAAS;GAAS,CAAC,EAAE;GAEtG"}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
|
|
3
|
-
//#region src/resources/index-resources.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* Register MCP resources.
|
|
6
|
-
*
|
|
7
|
-
* Single handler: mcp://backlog/{+path} → {BACKLOG_DATA_DIR}/{path}
|
|
8
|
-
*
|
|
9
|
-
* Examples:
|
|
10
|
-
* mcp://backlog/tasks/TASK-0092.md
|
|
11
|
-
* mcp://backlog/resources/TASK-0092/strategic-improvements.md
|
|
12
|
-
* mcp://backlog/backlog-mcp-engineer/system-config-visibility-2026-01-26/artifact.md
|
|
13
|
-
*
|
|
14
|
-
* For git repo files (ADRs, source code), use file:// URIs.
|
|
15
|
-
*/
|
|
16
|
-
declare function registerResources(server: McpServer): void;
|
|
17
|
-
//#endregion
|
|
18
|
-
export { registerResources };
|
|
19
|
-
//# sourceMappingURL=index-resources.d.mts.map
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { registerDataDirResource } from "./data-dir.mjs";
|
|
2
|
-
|
|
3
|
-
//#region src/resources/index-resources.ts
|
|
4
|
-
/**
|
|
5
|
-
* Register MCP resources.
|
|
6
|
-
*
|
|
7
|
-
* Single handler: mcp://backlog/{+path} → {BACKLOG_DATA_DIR}/{path}
|
|
8
|
-
*
|
|
9
|
-
* Examples:
|
|
10
|
-
* mcp://backlog/tasks/TASK-0092.md
|
|
11
|
-
* mcp://backlog/resources/TASK-0092/strategic-improvements.md
|
|
12
|
-
* mcp://backlog/backlog-mcp-engineer/system-config-visibility-2026-01-26/artifact.md
|
|
13
|
-
*
|
|
14
|
-
* For git repo files (ADRs, source code), use file:// URIs.
|
|
15
|
-
*/
|
|
16
|
-
function registerResources(server) {
|
|
17
|
-
registerDataDirResource(server);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
//#endregion
|
|
21
|
-
export { registerResources };
|
|
22
|
-
//# sourceMappingURL=index-resources.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index-resources.mjs","names":[],"sources":["../../src/resources/index-resources.ts"],"sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { registerDataDirResource } from './data-dir.js';\n\n/**\n * Register MCP resources.\n * \n * Single handler: mcp://backlog/{+path} → {BACKLOG_DATA_DIR}/{path}\n * \n * Examples:\n * mcp://backlog/tasks/TASK-0092.md\n * mcp://backlog/resources/TASK-0092/strategic-improvements.md\n * mcp://backlog/backlog-mcp-engineer/system-config-visibility-2026-01-26/artifact.md\n * \n * For git repo files (ADRs, source code), use file:// URIs.\n */\nexport function registerResources(server: McpServer) {\n registerDataDirResource(server);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,SAAgB,kBAAkB,QAAmB;AACnD,yBAAwB,OAAO"}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
|
|
3
|
-
//#region src/resources/resource-file.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* Repository resources (docs, ADRs, etc.) - NOT task-attached resources.
|
|
6
|
-
*
|
|
7
|
-
* URI Template (RFC 6570): mcp://backlog/resources/{+path}
|
|
8
|
-
* - {+path}: Greedy match - captures everything including slashes
|
|
9
|
-
* - Excludes TASK-/EPIC- prefixed paths (handled by task-attached.ts)
|
|
10
|
-
*
|
|
11
|
-
* Examples:
|
|
12
|
-
* ✅ mcp://backlog/resources/docs/adr/0001-decision.md
|
|
13
|
-
* ✅ mcp://backlog/resources/README.md
|
|
14
|
-
* ❌ mcp://backlog/resources/TASK-0092/file.md (handled by task-attached.ts)
|
|
15
|
-
*
|
|
16
|
-
* Storage location: {REPO_ROOT}/{path}
|
|
17
|
-
*/
|
|
18
|
-
declare function registerResourceFileResource(server: McpServer): void;
|
|
19
|
-
//#endregion
|
|
20
|
-
export { registerResourceFileResource };
|
|
21
|
-
//# sourceMappingURL=resource-file.d.mts.map
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { readMcpResource } from "./resource-reader.mjs";
|
|
2
|
-
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
|
|
4
|
-
//#region src/resources/resource-file.ts
|
|
5
|
-
/**
|
|
6
|
-
* Repository resources (docs, ADRs, etc.) - NOT task-attached resources.
|
|
7
|
-
*
|
|
8
|
-
* URI Template (RFC 6570): mcp://backlog/resources/{+path}
|
|
9
|
-
* - {+path}: Greedy match - captures everything including slashes
|
|
10
|
-
* - Excludes TASK-/EPIC- prefixed paths (handled by task-attached.ts)
|
|
11
|
-
*
|
|
12
|
-
* Examples:
|
|
13
|
-
* ✅ mcp://backlog/resources/docs/adr/0001-decision.md
|
|
14
|
-
* ✅ mcp://backlog/resources/README.md
|
|
15
|
-
* ❌ mcp://backlog/resources/TASK-0092/file.md (handled by task-attached.ts)
|
|
16
|
-
*
|
|
17
|
-
* Storage location: {REPO_ROOT}/{path}
|
|
18
|
-
*/
|
|
19
|
-
function registerResourceFileResource(server) {
|
|
20
|
-
const template = new ResourceTemplate("mcp://backlog/resources/{+path}", { list: void 0 });
|
|
21
|
-
server.registerResource("Resource File", template, {
|
|
22
|
-
description: "Repository resource files (docs, ADRs, etc.) - excludes task-attached resources",
|
|
23
|
-
mimeType: "text/plain"
|
|
24
|
-
}, async (uri, variables) => {
|
|
25
|
-
const path = String(variables.path);
|
|
26
|
-
if (/^(TASK-\d+|EPIC-\d+)\//.test(path)) throw new Error(`Task-attached resources must use the Task-Attached Resource handler. Path: ${path}`);
|
|
27
|
-
const resource = await readMcpResource(uri.toString());
|
|
28
|
-
return { contents: [{
|
|
29
|
-
uri: uri.toString(),
|
|
30
|
-
mimeType: resource.mimeType,
|
|
31
|
-
text: resource.content
|
|
32
|
-
}] };
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
//#endregion
|
|
37
|
-
export { registerResourceFileResource };
|
|
38
|
-
//# sourceMappingURL=resource-file.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"resource-file.mjs","names":[],"sources":["../../src/resources/resource-file.ts"],"sourcesContent":["import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { readMcpResource } from './resource-reader.js';\n\n/**\n * Repository resources (docs, ADRs, etc.) - NOT task-attached resources.\n * \n * URI Template (RFC 6570): mcp://backlog/resources/{+path}\n * - {+path}: Greedy match - captures everything including slashes\n * - Excludes TASK-/EPIC- prefixed paths (handled by task-attached.ts)\n * \n * Examples:\n * ✅ mcp://backlog/resources/docs/adr/0001-decision.md\n * ✅ mcp://backlog/resources/README.md\n * ❌ mcp://backlog/resources/TASK-0092/file.md (handled by task-attached.ts)\n * \n * Storage location: {REPO_ROOT}/{path}\n */\nexport function registerResourceFileResource(server: McpServer) {\n const template = new ResourceTemplate(\n 'mcp://backlog/resources/{+path}',\n { list: undefined } // No listing callback needed\n );\n \n server.registerResource(\n 'Resource File',\n template,\n { description: 'Repository resource files (docs, ADRs, etc.) - excludes task-attached resources', mimeType: 'text/plain' },\n async (uri, variables) => {\n const path = String(variables.path);\n \n // Reject task-attached resources - they have their own handler\n if (/^(TASK-\\d+|EPIC-\\d+)\\//.test(path)) {\n throw new Error(`Task-attached resources must use the Task-Attached Resource handler. Path: ${path}`);\n }\n \n const resource = await readMcpResource(uri.toString());\n return { contents: [{ uri: uri.toString(), mimeType: resource.mimeType, text: resource.content }] };\n }\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiBA,SAAgB,6BAA6B,QAAmB;CAC9D,MAAM,WAAW,IAAI,iBACnB,mCACA,EAAE,MAAM,QAAW,CACpB;AAED,QAAO,iBACL,iBACA,UACA;EAAE,aAAa;EAAmF,UAAU;EAAc,EAC1H,OAAO,KAAK,cAAc;EACxB,MAAM,OAAO,OAAO,UAAU,KAAK;AAGnC,MAAI,yBAAyB,KAAK,KAAK,CACrC,OAAM,IAAI,MAAM,8EAA8E,OAAO;EAGvG,MAAM,WAAW,MAAM,gBAAgB,IAAI,UAAU,CAAC;AACtD,SAAO,EAAE,UAAU,CAAC;GAAE,KAAK,IAAI,UAAU;GAAE,UAAU,SAAS;GAAU,MAAM,SAAS;GAAS,CAAC,EAAE;GAEtG"}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
//#region src/resources/resource-reader.d.ts
|
|
2
|
-
interface ResourceContent {
|
|
3
|
-
content: string;
|
|
4
|
-
frontmatter?: Record<string, any>;
|
|
5
|
-
mimeType: string;
|
|
6
|
-
}
|
|
7
|
-
declare function readMcpResource(uri: string): ResourceContent;
|
|
8
|
-
//#endregion
|
|
9
|
-
export { ResourceContent, readMcpResource };
|
|
10
|
-
//# sourceMappingURL=resource-reader.d.mts.map
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { resolveMcpUri } from "../utils/uri-resolver.mjs";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import matter from "gray-matter";
|
|
4
|
-
|
|
5
|
-
//#region src/resources/resource-reader.ts
|
|
6
|
-
function readMcpResource(uri) {
|
|
7
|
-
const filePath = resolveMcpUri(uri);
|
|
8
|
-
if (!existsSync(filePath)) throw new Error(`Resource not found: ${uri}`);
|
|
9
|
-
const content = readFileSync(filePath, "utf-8");
|
|
10
|
-
const ext = filePath.split(".").pop()?.toLowerCase() || "txt";
|
|
11
|
-
const mimeType = {
|
|
12
|
-
md: "text/markdown",
|
|
13
|
-
json: "application/json",
|
|
14
|
-
ts: "text/typescript",
|
|
15
|
-
js: "application/javascript",
|
|
16
|
-
txt: "text/plain"
|
|
17
|
-
}[ext] || "text/plain";
|
|
18
|
-
if (ext === "md") {
|
|
19
|
-
const parsed = matter(content);
|
|
20
|
-
return {
|
|
21
|
-
content: parsed.content,
|
|
22
|
-
frontmatter: parsed.data,
|
|
23
|
-
mimeType
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
return {
|
|
27
|
-
content,
|
|
28
|
-
mimeType
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
//#endregion
|
|
33
|
-
export { readMcpResource };
|
|
34
|
-
//# sourceMappingURL=resource-reader.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"resource-reader.mjs","names":[],"sources":["../../src/resources/resource-reader.ts"],"sourcesContent":["import { readFileSync, existsSync } from 'node:fs';\nimport matter from 'gray-matter';\nimport { resolveMcpUri } from '../utils/uri-resolver.js';\n\nexport interface ResourceContent {\n content: string;\n frontmatter?: Record<string, any>;\n mimeType: string;\n}\n\nexport function readMcpResource(uri: string): ResourceContent {\n const filePath = resolveMcpUri(uri);\n \n if (!existsSync(filePath)) {\n throw new Error(`Resource not found: ${uri}`);\n }\n \n const content = readFileSync(filePath, 'utf-8');\n const ext = filePath.split('.').pop()?.toLowerCase() || 'txt';\n \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 const mimeType = mimeMap[ext] || 'text/plain';\n \n // Parse frontmatter for markdown files\n if (ext === 'md') {\n const parsed = matter(content);\n return {\n content: parsed.content,\n frontmatter: parsed.data,\n mimeType,\n };\n }\n \n return {\n content,\n mimeType,\n };\n}\n"],"mappings":";;;;;AAUA,SAAgB,gBAAgB,KAA8B;CAC5D,MAAM,WAAW,cAAc,IAAI;AAEnC,KAAI,CAAC,WAAW,SAAS,CACvB,OAAM,IAAI,MAAM,uBAAuB,MAAM;CAG/C,MAAM,UAAU,aAAa,UAAU,QAAQ;CAC/C,MAAM,MAAM,SAAS,MAAM,IAAI,CAAC,KAAK,EAAE,aAAa,IAAI;CAUxD,MAAM,WARkC;EACtC,IAAI;EACJ,MAAM;EACN,IAAI;EACJ,IAAI;EACJ,KAAK;EACN,CAEwB,QAAQ;AAGjC,KAAI,QAAQ,MAAM;EAChB,MAAM,SAAS,OAAO,QAAQ;AAC9B,SAAO;GACL,SAAS,OAAO;GAChB,aAAa,OAAO;GACpB;GACD;;AAGH,QAAO;EACL;EACA;EACD"}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
|
|
3
|
-
//#region src/resources/task-attached.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* Task-attached resources (ADRs, design docs, artifacts specific to a task).
|
|
6
|
-
*
|
|
7
|
-
* URI Template (RFC 6570): mcp://backlog/resources/{taskId}/{filename}
|
|
8
|
-
* - {taskId}: TASK-NNNN or EPIC-NNNN
|
|
9
|
-
* - {filename}: Any filename (no slashes)
|
|
10
|
-
*
|
|
11
|
-
* Examples:
|
|
12
|
-
* ✅ mcp://backlog/resources/TASK-0092/strategic-improvements.md
|
|
13
|
-
* ✅ mcp://backlog/resources/EPIC-0002/roadmap.md
|
|
14
|
-
* ❌ mcp://backlog/resources/docs/adr/0001.md (handled by resource-file.ts)
|
|
15
|
-
*
|
|
16
|
-
* Storage location: {BACKLOG_DATA_DIR}/resources/{taskId}/{filename}
|
|
17
|
-
*/
|
|
18
|
-
declare function registerTaskAttachedResource(server: McpServer): void;
|
|
19
|
-
//#endregion
|
|
20
|
-
export { registerTaskAttachedResource };
|
|
21
|
-
//# sourceMappingURL=task-attached.d.mts.map
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { readMcpResource } from "./resource-reader.mjs";
|
|
2
|
-
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
|
|
4
|
-
//#region src/resources/task-attached.ts
|
|
5
|
-
/**
|
|
6
|
-
* Task-attached resources (ADRs, design docs, artifacts specific to a task).
|
|
7
|
-
*
|
|
8
|
-
* URI Template (RFC 6570): mcp://backlog/resources/{taskId}/{filename}
|
|
9
|
-
* - {taskId}: TASK-NNNN or EPIC-NNNN
|
|
10
|
-
* - {filename}: Any filename (no slashes)
|
|
11
|
-
*
|
|
12
|
-
* Examples:
|
|
13
|
-
* ✅ mcp://backlog/resources/TASK-0092/strategic-improvements.md
|
|
14
|
-
* ✅ mcp://backlog/resources/EPIC-0002/roadmap.md
|
|
15
|
-
* ❌ mcp://backlog/resources/docs/adr/0001.md (handled by resource-file.ts)
|
|
16
|
-
*
|
|
17
|
-
* Storage location: {BACKLOG_DATA_DIR}/resources/{taskId}/{filename}
|
|
18
|
-
*/
|
|
19
|
-
function registerTaskAttachedResource(server) {
|
|
20
|
-
const template = new ResourceTemplate("mcp://backlog/resources/{taskId}/{filename}", { list: void 0 });
|
|
21
|
-
server.registerResource("Task-Attached Resource", template, { description: "Task-attached resources (ADRs, design docs, etc.)" }, async (uri, variables) => {
|
|
22
|
-
const taskId = String(variables.taskId);
|
|
23
|
-
String(variables.filename);
|
|
24
|
-
if (!/^(TASK-\d+|EPIC-\d+)$/.test(taskId)) throw new Error(`Invalid task ID format. Expected TASK-NNNN or EPIC-NNNN, got: ${taskId}`);
|
|
25
|
-
const { content, mimeType } = await readMcpResource(uri.toString());
|
|
26
|
-
return { contents: [{
|
|
27
|
-
uri: uri.toString(),
|
|
28
|
-
mimeType,
|
|
29
|
-
text: content
|
|
30
|
-
}] };
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
//#endregion
|
|
35
|
-
export { registerTaskAttachedResource };
|
|
36
|
-
//# sourceMappingURL=task-attached.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"task-attached.mjs","names":[],"sources":["../../src/resources/task-attached.ts"],"sourcesContent":["import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { readMcpResource } from './resource-reader.js';\n\n/**\n * Task-attached resources (ADRs, design docs, artifacts specific to a task).\n * \n * URI Template (RFC 6570): mcp://backlog/resources/{taskId}/{filename}\n * - {taskId}: TASK-NNNN or EPIC-NNNN\n * - {filename}: Any filename (no slashes)\n * \n * Examples:\n * ✅ mcp://backlog/resources/TASK-0092/strategic-improvements.md\n * ✅ mcp://backlog/resources/EPIC-0002/roadmap.md\n * ❌ mcp://backlog/resources/docs/adr/0001.md (handled by resource-file.ts)\n * \n * Storage location: {BACKLOG_DATA_DIR}/resources/{taskId}/{filename}\n */\nexport function registerTaskAttachedResource(server: McpServer) {\n const template = new ResourceTemplate(\n 'mcp://backlog/resources/{taskId}/{filename}',\n { list: undefined } // No listing callback needed\n );\n \n server.registerResource(\n 'Task-Attached Resource',\n template,\n { description: 'Task-attached resources (ADRs, design docs, etc.)' },\n async (uri, variables) => {\n const taskId = String(variables.taskId);\n const filename = String(variables.filename);\n \n // Validate task ID format\n if (!/^(TASK-\\d+|EPIC-\\d+)$/.test(taskId)) {\n throw new Error(`Invalid task ID format. Expected TASK-NNNN or EPIC-NNNN, got: ${taskId}`);\n }\n \n const { content, mimeType } = await readMcpResource(uri.toString());\n return { contents: [{ uri: uri.toString(), mimeType, text: content }] };\n }\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiBA,SAAgB,6BAA6B,QAAmB;CAC9D,MAAM,WAAW,IAAI,iBACnB,+CACA,EAAE,MAAM,QAAW,CACpB;AAED,QAAO,iBACL,0BACA,UACA,EAAE,aAAa,qDAAqD,EACpE,OAAO,KAAK,cAAc;EACxB,MAAM,SAAS,OAAO,UAAU,OAAO;AACtB,SAAO,UAAU,SAAS;AAG3C,MAAI,CAAC,wBAAwB,KAAK,OAAO,CACvC,OAAM,IAAI,MAAM,iEAAiE,SAAS;EAG5F,MAAM,EAAE,SAAS,aAAa,MAAM,gBAAgB,IAAI,UAAU,CAAC;AACnE,SAAO,EAAE,UAAU,CAAC;GAAE,KAAK,IAAI,UAAU;GAAE;GAAU,MAAM;GAAS,CAAC,EAAE;GAE1E"}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
|
|
3
|
-
//#region src/resources/task-by-id.d.ts
|
|
4
|
-
declare function registerTaskByIdResource(server: McpServer): void;
|
|
5
|
-
//#endregion
|
|
6
|
-
export { registerTaskByIdResource };
|
|
7
|
-
//# sourceMappingURL=task-by-id.d.mts.map
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { storage } from "../storage/backlog.mjs";
|
|
2
|
-
|
|
3
|
-
//#region src/resources/task-by-id.ts
|
|
4
|
-
function registerTaskByIdResource(server) {
|
|
5
|
-
server.registerResource("Task by ID", "mcp://backlog/tasks/{taskId}/file", {
|
|
6
|
-
description: "Get a specific task",
|
|
7
|
-
mimeType: "text/markdown"
|
|
8
|
-
}, async (uri) => {
|
|
9
|
-
const match = uri.toString().match(/mcp:\/\/backlog\/tasks\/([^/]+)\/file/);
|
|
10
|
-
if (!match || !match[1]) throw new Error("Invalid URI");
|
|
11
|
-
const markdown = storage.getMarkdown(match[1]);
|
|
12
|
-
if (!markdown) throw new Error("Task not found");
|
|
13
|
-
return { contents: [{
|
|
14
|
-
uri: uri.toString(),
|
|
15
|
-
mimeType: "text/markdown",
|
|
16
|
-
text: markdown
|
|
17
|
-
}] };
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
//#endregion
|
|
22
|
-
export { registerTaskByIdResource };
|
|
23
|
-
//# sourceMappingURL=task-by-id.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"task-by-id.mjs","names":[],"sources":["../../src/resources/task-by-id.ts"],"sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { storage } from '../storage/backlog.js';\n\nexport function registerTaskByIdResource(server: McpServer) {\n server.registerResource(\n 'Task by ID',\n 'mcp://backlog/tasks/{taskId}/file',\n { description: 'Get a specific task', mimeType: 'text/markdown' },\n async (uri: URL) => {\n const match = uri.toString().match(/mcp:\\/\\/backlog\\/tasks\\/([^/]+)\\/file/);\n if (!match || !match[1]) throw new Error('Invalid URI');\n const markdown = storage.getMarkdown(match[1]);\n if (!markdown) throw new Error('Task not found');\n return { contents: [{ uri: uri.toString(), mimeType: 'text/markdown', text: markdown }] };\n }\n );\n}\n"],"mappings":";;;AAGA,SAAgB,yBAAyB,QAAmB;AAC1D,QAAO,iBACL,cACA,qCACA;EAAE,aAAa;EAAuB,UAAU;EAAiB,EACjE,OAAO,QAAa;EAClB,MAAM,QAAQ,IAAI,UAAU,CAAC,MAAM,wCAAwC;AAC3E,MAAI,CAAC,SAAS,CAAC,MAAM,GAAI,OAAM,IAAI,MAAM,cAAc;EACvD,MAAM,WAAW,QAAQ,YAAY,MAAM,GAAG;AAC9C,MAAI,CAAC,SAAU,OAAM,IAAI,MAAM,iBAAiB;AAChD,SAAO,EAAE,UAAU,CAAC;GAAE,KAAK,IAAI,UAAU;GAAE,UAAU;GAAiB,MAAM;GAAU,CAAC,EAAE;GAE5F"}
|
package/dist/resources/tasks.mjs
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { storage } from "../storage/backlog.mjs";
|
|
2
|
-
|
|
3
|
-
//#region src/resources/tasks.ts
|
|
4
|
-
function registerTasksResource(server) {
|
|
5
|
-
server.registerResource("All Tasks", "mcp://backlog/tasks", {
|
|
6
|
-
description: "List of all tasks",
|
|
7
|
-
mimeType: "application/json"
|
|
8
|
-
}, async () => {
|
|
9
|
-
const tasks = storage.list({});
|
|
10
|
-
return { contents: [{
|
|
11
|
-
uri: "mcp://backlog/tasks",
|
|
12
|
-
mimeType: "application/json",
|
|
13
|
-
text: JSON.stringify(tasks, null, 2)
|
|
14
|
-
}] };
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
//#endregion
|
|
19
|
-
export { registerTasksResource };
|
|
20
|
-
//# sourceMappingURL=tasks.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tasks.mjs","names":[],"sources":["../../src/resources/tasks.ts"],"sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { storage } from '../storage/backlog.js';\n\nexport function registerTasksResource(server: McpServer) {\n server.registerResource(\n 'All Tasks',\n 'mcp://backlog/tasks',\n { description: 'List of all tasks', mimeType: 'application/json' },\n async () => {\n const tasks = storage.list({});\n return { contents: [{ uri: 'mcp://backlog/tasks', mimeType: 'application/json', text: JSON.stringify(tasks, null, 2) }] };\n }\n );\n}\n"],"mappings":";;;AAGA,SAAgB,sBAAsB,QAAmB;AACvD,QAAO,iBACL,aACA,uBACA;EAAE,aAAa;EAAqB,UAAU;EAAoB,EAClE,YAAY;EACV,MAAM,QAAQ,QAAQ,KAAK,EAAE,CAAC;AAC9B,SAAO,EAAE,UAAU,CAAC;GAAE,KAAK;GAAuB,UAAU;GAAoB,MAAM,KAAK,UAAU,OAAO,MAAM,EAAE;GAAE,CAAC,EAAE;GAE5H"}
|
package/dist/resources/uri.d.mts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
//#region src/resources/uri.d.ts
|
|
2
|
-
interface ParsedURI {
|
|
3
|
-
server: string;
|
|
4
|
-
resource: string;
|
|
5
|
-
taskId?: string;
|
|
6
|
-
field?: string;
|
|
7
|
-
}
|
|
8
|
-
declare function parseURI(uri: string): ParsedURI | null;
|
|
9
|
-
//#endregion
|
|
10
|
-
export { ParsedURI, parseURI };
|
|
11
|
-
//# sourceMappingURL=uri.d.mts.map
|
package/dist/resources/uri.mjs
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
//#region src/resources/uri.ts
|
|
2
|
-
function parseURI(uri) {
|
|
3
|
-
const match = uri.match(/^mcp:\/\/([^\/]+)\/(.+)$/);
|
|
4
|
-
if (!match) return null;
|
|
5
|
-
const [, server, resource] = match;
|
|
6
|
-
if (!server || !resource) return null;
|
|
7
|
-
const taskMatch = resource.match(/^tasks\/([^\/]+)(?:\/(description|file))?$/);
|
|
8
|
-
if (taskMatch) return {
|
|
9
|
-
server,
|
|
10
|
-
resource,
|
|
11
|
-
taskId: taskMatch[1],
|
|
12
|
-
field: taskMatch[2] || "file"
|
|
13
|
-
};
|
|
14
|
-
return {
|
|
15
|
-
server,
|
|
16
|
-
resource
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
//#endregion
|
|
21
|
-
export { parseURI };
|
|
22
|
-
//# sourceMappingURL=uri.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"uri.mjs","names":[],"sources":["../../src/resources/uri.ts"],"sourcesContent":["// URI parser for mcp:// scheme\n\nexport interface ParsedURI {\n server: string;\n resource: string;\n taskId?: string;\n field?: string;\n}\n\nexport function parseURI(uri: string): ParsedURI | null {\n // Expected format: mcp://backlog/{resource}\n const match = uri.match(/^mcp:\\/\\/([^\\/]+)\\/(.+)$/);\n if (!match) return null;\n\n const [, server, resource] = match;\n \n if (!server || !resource) return null;\n \n // Check if it's a task field edit: tasks/{id}/description or tasks/{id}/file\n const taskMatch = resource.match(/^tasks\\/([^\\/]+)(?:\\/(description|file))?$/);\n if (taskMatch) {\n return {\n server,\n resource,\n taskId: taskMatch[1],\n field: taskMatch[2] || 'file',\n };\n }\n\n // General resource (artifacts, resources, etc.)\n return {\n server,\n resource,\n };\n}\n"],"mappings":";AASA,SAAgB,SAAS,KAA+B;CAEtD,MAAM,QAAQ,IAAI,MAAM,2BAA2B;AACnD,KAAI,CAAC,MAAO,QAAO;CAEnB,MAAM,GAAG,QAAQ,YAAY;AAE7B,KAAI,CAAC,UAAU,CAAC,SAAU,QAAO;CAGjC,MAAM,YAAY,SAAS,MAAM,6CAA6C;AAC9E,KAAI,UACF,QAAO;EACL;EACA;EACA,QAAQ,UAAU;EAClB,OAAO,UAAU,MAAM;EACxB;AAIH,QAAO;EACL;EACA;EACD"}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { Operation, WriteResourceResult } from "./types.mjs";
|
|
2
|
-
|
|
3
|
-
//#region src/resources/write.d.ts
|
|
4
|
-
interface WriteResourceParams {
|
|
5
|
-
uri: string;
|
|
6
|
-
operation: Operation;
|
|
7
|
-
}
|
|
8
|
-
declare function writeResource(params: WriteResourceParams, getFilePath: (taskId: string) => string | null, resolvePath: (uri: string) => string): WriteResourceResult;
|
|
9
|
-
//#endregion
|
|
10
|
-
export { WriteResourceParams, writeResource };
|
|
11
|
-
//# sourceMappingURL=write.d.mts.map
|
package/dist/resources/write.mjs
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { parseURI } from "./uri.mjs";
|
|
2
|
-
import { applyOperation } from "./operations.mjs";
|
|
3
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
4
|
-
import matter from "gray-matter";
|
|
5
|
-
|
|
6
|
-
//#region src/resources/write.ts
|
|
7
|
-
function writeResource(params, getFilePath, resolvePath) {
|
|
8
|
-
try {
|
|
9
|
-
const parsed = parseURI(params.uri);
|
|
10
|
-
if (!parsed) return {
|
|
11
|
-
success: false,
|
|
12
|
-
message: "Invalid URI format",
|
|
13
|
-
error: "Expected format: mcp://backlog/..."
|
|
14
|
-
};
|
|
15
|
-
if (parsed.server !== "backlog") return {
|
|
16
|
-
success: false,
|
|
17
|
-
message: `Unknown server: ${parsed.server}`,
|
|
18
|
-
error: "Only \"backlog\" server is supported"
|
|
19
|
-
};
|
|
20
|
-
if (parsed.taskId && parsed.field) {
|
|
21
|
-
const filePath = getFilePath(parsed.taskId);
|
|
22
|
-
if (!filePath) return {
|
|
23
|
-
success: false,
|
|
24
|
-
message: `Task not found: ${parsed.taskId}`,
|
|
25
|
-
error: `No file found for task ${parsed.taskId}`
|
|
26
|
-
};
|
|
27
|
-
const fileContent = readFileSync(filePath, "utf-8");
|
|
28
|
-
const { data: frontmatter, content: description } = matter(fileContent);
|
|
29
|
-
let newContent;
|
|
30
|
-
if (parsed.field === "description") {
|
|
31
|
-
newContent = applyOperation(description, params.operation);
|
|
32
|
-
writeFileSync(filePath, matter.stringify(newContent, frontmatter), "utf-8");
|
|
33
|
-
} else if (parsed.field === "file") {
|
|
34
|
-
newContent = applyOperation(fileContent, params.operation);
|
|
35
|
-
writeFileSync(filePath, newContent, "utf-8");
|
|
36
|
-
} else return {
|
|
37
|
-
success: false,
|
|
38
|
-
message: `Unsupported field: ${parsed.field}`,
|
|
39
|
-
error: "Only \"description\" and \"file\" fields are supported for editing"
|
|
40
|
-
};
|
|
41
|
-
return {
|
|
42
|
-
success: true,
|
|
43
|
-
message: `Successfully applied ${params.operation.type} to ${params.uri}`
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
const filePath = resolvePath(params.uri);
|
|
47
|
-
writeFileSync(filePath, applyOperation(readFileSync(filePath, "utf-8"), params.operation), "utf-8");
|
|
48
|
-
return {
|
|
49
|
-
success: true,
|
|
50
|
-
message: `Successfully applied ${params.operation.type} to ${params.uri}`
|
|
51
|
-
};
|
|
52
|
-
} catch (error) {
|
|
53
|
-
return {
|
|
54
|
-
success: false,
|
|
55
|
-
message: "Operation failed",
|
|
56
|
-
error: error instanceof Error ? error.message : String(error)
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
//#endregion
|
|
62
|
-
export { writeResource };
|
|
63
|
-
//# sourceMappingURL=write.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"write.mjs","names":[],"sources":["../../src/resources/write.ts"],"sourcesContent":["// Main write_resource implementation\n\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport matter from 'gray-matter';\nimport type { Operation, WriteResourceResult } from './types.js';\nimport { parseURI } from './uri.js';\nimport { applyOperation } from './operations.js';\n\nexport interface WriteResourceParams {\n uri: string;\n operation: Operation;\n}\n\nexport function writeResource(\n params: WriteResourceParams,\n getFilePath: (taskId: string) => string | null,\n resolvePath: (uri: string) => string\n): WriteResourceResult {\n try {\n // Parse URI\n const parsed = parseURI(params.uri);\n if (!parsed) {\n return {\n success: false,\n message: 'Invalid URI format',\n error: 'Expected format: mcp://backlog/...',\n };\n }\n\n if (parsed.server !== 'backlog') {\n return {\n success: false,\n message: `Unknown server: ${parsed.server}`,\n error: 'Only \"backlog\" server is supported',\n };\n }\n\n // Handle task field edits (description/file)\n if (parsed.taskId && parsed.field) {\n const filePath = getFilePath(parsed.taskId);\n if (!filePath) {\n return {\n success: false,\n message: `Task not found: ${parsed.taskId}`,\n error: `No file found for task ${parsed.taskId}`,\n };\n }\n\n const fileContent = readFileSync(filePath, 'utf-8');\n const { data: frontmatter, content: description } = matter(fileContent);\n\n let newContent: string;\n \n if (parsed.field === 'description') {\n newContent = applyOperation(description, params.operation);\n const newFile = matter.stringify(newContent, frontmatter);\n writeFileSync(filePath, newFile, 'utf-8');\n } else if (parsed.field === 'file') {\n newContent = applyOperation(fileContent, params.operation);\n writeFileSync(filePath, newContent, 'utf-8');\n } else {\n return {\n success: false,\n message: `Unsupported field: ${parsed.field}`,\n error: 'Only \"description\" and \"file\" fields are supported for editing',\n };\n }\n\n return {\n success: true,\n message: `Successfully applied ${params.operation.type} to ${params.uri}`,\n };\n }\n\n // Handle general file operations (artifacts, resources, etc.)\n const filePath = resolvePath(params.uri);\n const fileContent = readFileSync(filePath, 'utf-8');\n const newContent = applyOperation(fileContent, params.operation);\n writeFileSync(filePath, newContent, 'utf-8');\n\n return {\n success: true,\n message: `Successfully applied ${params.operation.type} to ${params.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"],"mappings":";;;;;;AAaA,SAAgB,cACd,QACA,aACA,aACqB;AACrB,KAAI;EAEF,MAAM,SAAS,SAAS,OAAO,IAAI;AACnC,MAAI,CAAC,OACH,QAAO;GACL,SAAS;GACT,SAAS;GACT,OAAO;GACR;AAGH,MAAI,OAAO,WAAW,UACpB,QAAO;GACL,SAAS;GACT,SAAS,mBAAmB,OAAO;GACnC,OAAO;GACR;AAIH,MAAI,OAAO,UAAU,OAAO,OAAO;GACjC,MAAM,WAAW,YAAY,OAAO,OAAO;AAC3C,OAAI,CAAC,SACH,QAAO;IACL,SAAS;IACT,SAAS,mBAAmB,OAAO;IACnC,OAAO,0BAA0B,OAAO;IACzC;GAGH,MAAM,cAAc,aAAa,UAAU,QAAQ;GACnD,MAAM,EAAE,MAAM,aAAa,SAAS,gBAAgB,OAAO,YAAY;GAEvE,IAAI;AAEJ,OAAI,OAAO,UAAU,eAAe;AAClC,iBAAa,eAAe,aAAa,OAAO,UAAU;AAE1D,kBAAc,UADE,OAAO,UAAU,YAAY,YAAY,EACxB,QAAQ;cAChC,OAAO,UAAU,QAAQ;AAClC,iBAAa,eAAe,aAAa,OAAO,UAAU;AAC1D,kBAAc,UAAU,YAAY,QAAQ;SAE5C,QAAO;IACL,SAAS;IACT,SAAS,sBAAsB,OAAO;IACtC,OAAO;IACR;AAGH,UAAO;IACL,SAAS;IACT,SAAS,wBAAwB,OAAO,UAAU,KAAK,MAAM,OAAO;IACrE;;EAIH,MAAM,WAAW,YAAY,OAAO,IAAI;AAGxC,gBAAc,UADK,eADC,aAAa,UAAU,QAAQ,EACJ,OAAO,UAAU,EAC5B,QAAQ;AAE5C,SAAO;GACL,SAAS;GACT,SAAS,wBAAwB,OAAO,UAAU,KAAK,MAAM,OAAO;GACrE;UACM,OAAO;AACd,SAAO;GACL,SAAS;GACT,SAAS;GACT,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;GAC9D"}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
//#region src/utils/uri-resolver.d.ts
|
|
2
|
-
declare function getRepoRoot(): string;
|
|
3
|
-
declare function resolveMcpUri(uri: string): string;
|
|
4
|
-
declare function filePathToMcpUri(filePath: string): string | null;
|
|
5
|
-
//#endregion
|
|
6
|
-
export { filePathToMcpUri, getRepoRoot, resolveMcpUri };
|
|
7
|
-
//# sourceMappingURL=uri-resolver.d.mts.map
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { paths } from "./paths.mjs";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
|
|
5
|
-
//#region src/utils/uri-resolver.ts
|
|
6
|
-
function getRepoRoot() {
|
|
7
|
-
return paths.projectRoot;
|
|
8
|
-
}
|
|
9
|
-
function resolveMcpUri(uri) {
|
|
10
|
-
if (!uri.startsWith("mcp://")) throw new Error(`Not an MCP URI: ${uri}`);
|
|
11
|
-
const url = new URL(uri);
|
|
12
|
-
if (url.hostname !== "backlog") throw new Error(`Invalid MCP URI hostname: ${url.hostname}`);
|
|
13
|
-
const path = url.pathname.substring(1);
|
|
14
|
-
if (path.includes("..")) throw new Error(`Path traversal not allowed: ${uri}`);
|
|
15
|
-
const dataDir = paths.backlogDataDir;
|
|
16
|
-
if (path.startsWith("tasks/")) {
|
|
17
|
-
const match = path.match(/^tasks\/([^/]+)(\/(?:file|description))?$/);
|
|
18
|
-
if (match && match[1] && !match[1].endsWith(".md")) return join(dataDir, "tasks", `${match[1]}.md`);
|
|
19
|
-
}
|
|
20
|
-
if (path.startsWith("resources/")) if (path.match(/^resources\/(TASK-\d+|EPIC-\d+)\//)) return join(dataDir, path);
|
|
21
|
-
else {
|
|
22
|
-
const repoPath = path.substring(10);
|
|
23
|
-
return join(getRepoRoot(), repoPath);
|
|
24
|
-
}
|
|
25
|
-
return join(dataDir, path);
|
|
26
|
-
}
|
|
27
|
-
function filePathToMcpUri(filePath) {
|
|
28
|
-
const dataDir = paths.backlogDataDir;
|
|
29
|
-
const repoRoot = getRepoRoot();
|
|
30
|
-
if (filePath.includes(`${dataDir}/tasks/`)) {
|
|
31
|
-
const match = filePath.match(/(TASK-\d+|EPIC-\d+)\.md$/);
|
|
32
|
-
if (match) return `mcp://backlog/tasks/${match[1]}`;
|
|
33
|
-
}
|
|
34
|
-
if (filePath.startsWith(repoRoot)) return `mcp://backlog/resources/${filePath.substring(repoRoot.length + 1)}`;
|
|
35
|
-
const dataDirParent = dirname(dataDir);
|
|
36
|
-
if (filePath.startsWith(dataDirParent) && !filePath.startsWith(dataDir)) return `mcp://backlog/artifacts/${filePath.substring(dataDirParent.length + 1)}`;
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
//#endregion
|
|
41
|
-
export { filePathToMcpUri, getRepoRoot, resolveMcpUri };
|
|
42
|
-
//# sourceMappingURL=uri-resolver.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"uri-resolver.mjs","names":[],"sources":["../../src/utils/uri-resolver.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport { paths } from '@/utils/paths.js';\n\nexport function getRepoRoot(): string {\n return paths.projectRoot;\n}\n\nexport function resolveMcpUri(uri: string): string {\n if (!uri.startsWith('mcp://')) {\n throw new Error(`Not an MCP URI: ${uri}`);\n }\n\n const url = new URL(uri);\n \n if (url.hostname !== 'backlog') {\n throw new Error(`Invalid MCP URI hostname: ${url.hostname}`);\n }\n \n const path = url.pathname.substring(1); // Remove leading /\n \n if (path.includes('..')) {\n throw new Error(`Path traversal not allowed: ${uri}`);\n }\n \n const dataDir = paths.backlogDataDir;\n \n // Special case: tasks/{id} or tasks/{id}/file -> tasks/{id}.md\n // Only applies if path doesn't already have .md extension\n if (path.startsWith('tasks/')) {\n const match = path.match(/^tasks\\/([^/]+)(\\/(?:file|description))?$/);\n if (match && match[1] && !match[1].endsWith('.md')) {\n return join(dataDir, 'tasks', `${match[1]}.md`);\n }\n }\n \n // Special case: resources/{TASK-XXXX} or resources/{EPIC-XXXX} -> task-attached resources\n if (path.startsWith('resources/')) {\n const match = path.match(/^resources\\/(TASK-\\d+|EPIC-\\d+)\\//);\n if (match) {\n // Task-attached resource: dataDir/resources/{taskId}/{file}\n return join(dataDir, path);\n } else {\n // Repository resource: repoRoot/{path after resources/}\n const repoPath = path.substring('resources/'.length);\n return join(getRepoRoot(), repoPath);\n }\n }\n \n // Everything else: direct mapping to dataDir/{path}\n return join(dataDir, path);\n}\n\nexport function filePathToMcpUri(filePath: string): string | null {\n const dataDir = paths.backlogDataDir;\n const repoRoot = getRepoRoot();\n \n // Check if it's a task file\n if (filePath.includes(`${dataDir}/tasks/`)) {\n const match = filePath.match(/(TASK-\\d+|EPIC-\\d+)\\.md$/);\n if (match) {\n return `mcp://backlog/tasks/${match[1]}`;\n }\n }\n \n // Check if it's a repo resource\n if (filePath.startsWith(repoRoot)) {\n const relativePath = filePath.substring(repoRoot.length + 1);\n return `mcp://backlog/resources/${relativePath}`;\n }\n \n // Check if it's an artifact\n const dataDirParent = dirname(dataDir);\n if (filePath.startsWith(dataDirParent) && !filePath.startsWith(dataDir)) {\n const relativePath = filePath.substring(dataDirParent.length + 1);\n return `mcp://backlog/artifacts/${relativePath}`;\n }\n \n return null;\n}\n"],"mappings":";;;;;AAIA,SAAgB,cAAsB;AACpC,QAAO,MAAM;;AAGf,SAAgB,cAAc,KAAqB;AACjD,KAAI,CAAC,IAAI,WAAW,SAAS,CAC3B,OAAM,IAAI,MAAM,mBAAmB,MAAM;CAG3C,MAAM,MAAM,IAAI,IAAI,IAAI;AAExB,KAAI,IAAI,aAAa,UACnB,OAAM,IAAI,MAAM,6BAA6B,IAAI,WAAW;CAG9D,MAAM,OAAO,IAAI,SAAS,UAAU,EAAE;AAEtC,KAAI,KAAK,SAAS,KAAK,CACrB,OAAM,IAAI,MAAM,+BAA+B,MAAM;CAGvD,MAAM,UAAU,MAAM;AAItB,KAAI,KAAK,WAAW,SAAS,EAAE;EAC7B,MAAM,QAAQ,KAAK,MAAM,4CAA4C;AACrE,MAAI,SAAS,MAAM,MAAM,CAAC,MAAM,GAAG,SAAS,MAAM,CAChD,QAAO,KAAK,SAAS,SAAS,GAAG,MAAM,GAAG,KAAK;;AAKnD,KAAI,KAAK,WAAW,aAAa,CAE/B,KADc,KAAK,MAAM,oCAAoC,CAG3D,QAAO,KAAK,SAAS,KAAK;MACrB;EAEL,MAAM,WAAW,KAAK,UAAU,GAAoB;AACpD,SAAO,KAAK,aAAa,EAAE,SAAS;;AAKxC,QAAO,KAAK,SAAS,KAAK;;AAG5B,SAAgB,iBAAiB,UAAiC;CAChE,MAAM,UAAU,MAAM;CACtB,MAAM,WAAW,aAAa;AAG9B,KAAI,SAAS,SAAS,GAAG,QAAQ,SAAS,EAAE;EAC1C,MAAM,QAAQ,SAAS,MAAM,2BAA2B;AACxD,MAAI,MACF,QAAO,uBAAuB,MAAM;;AAKxC,KAAI,SAAS,WAAW,SAAS,CAE/B,QAAO,2BADc,SAAS,UAAU,SAAS,SAAS,EAAE;CAK9D,MAAM,gBAAgB,QAAQ,QAAQ;AACtC,KAAI,SAAS,WAAW,cAAc,IAAI,CAAC,SAAS,WAAW,QAAQ,CAErE,QAAO,2BADc,SAAS,UAAU,cAAc,SAAS,EAAE;AAInE,QAAO"}
|