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.
Files changed (41) hide show
  1. package/README.md +3 -3
  2. package/dist/resources/index.d.mts +2 -2
  3. package/dist/resources/index.mjs +2 -2
  4. package/dist/resources/manager.d.mts +71 -0
  5. package/dist/resources/manager.mjs +186 -0
  6. package/dist/resources/manager.mjs.map +1 -0
  7. package/dist/server/mcp-handler.mjs +3 -2
  8. package/dist/server/mcp-handler.mjs.map +1 -1
  9. package/dist/server/viewer-routes.mjs +4 -5
  10. package/dist/server/viewer-routes.mjs.map +1 -1
  11. package/package.json +1 -1
  12. package/dist/resources/data-dir.d.mts +0 -23
  13. package/dist/resources/data-dir.mjs +0 -36
  14. package/dist/resources/data-dir.mjs.map +0 -1
  15. package/dist/resources/index-resources.d.mts +0 -19
  16. package/dist/resources/index-resources.mjs +0 -22
  17. package/dist/resources/index-resources.mjs.map +0 -1
  18. package/dist/resources/resource-file.d.mts +0 -21
  19. package/dist/resources/resource-file.mjs +0 -38
  20. package/dist/resources/resource-file.mjs.map +0 -1
  21. package/dist/resources/resource-reader.d.mts +0 -10
  22. package/dist/resources/resource-reader.mjs +0 -34
  23. package/dist/resources/resource-reader.mjs.map +0 -1
  24. package/dist/resources/task-attached.d.mts +0 -21
  25. package/dist/resources/task-attached.mjs +0 -36
  26. package/dist/resources/task-attached.mjs.map +0 -1
  27. package/dist/resources/task-by-id.d.mts +0 -7
  28. package/dist/resources/task-by-id.mjs +0 -23
  29. package/dist/resources/task-by-id.mjs.map +0 -1
  30. package/dist/resources/tasks.d.mts +0 -7
  31. package/dist/resources/tasks.mjs +0 -20
  32. package/dist/resources/tasks.mjs.map +0 -1
  33. package/dist/resources/uri.d.mts +0 -11
  34. package/dist/resources/uri.mjs +0 -22
  35. package/dist/resources/uri.mjs.map +0 -1
  36. package/dist/resources/write.d.mts +0 -11
  37. package/dist/resources/write.mjs +0 -63
  38. package/dist/resources/write.mjs.map +0 -1
  39. package/dist/utils/uri-resolver.d.mts +0 -7
  40. package/dist/utils/uri-resolver.mjs +0 -42
  41. 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 # All tasks (JSON)
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
- Create/modify resources via `resource://` write operations.
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 { writeResource } from "./write.mjs";
3
- export { type Operation, type WriteResourceResult, writeResource };
2
+ import { ResourceContent, ResourceManager, resourceManager } from "./manager.mjs";
3
+ export { type Operation, type ResourceContent, ResourceManager, type WriteResourceResult, resourceManager };
@@ -1,3 +1,3 @@
1
- import { writeResource } from "./write.mjs";
1
+ import { ResourceManager, resourceManager } from "./manager.mjs";
2
2
 
3
- export { writeResource };
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
- registerResources(server);
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 { registerResources } from '@/resources/index-resources.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 registerResources(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,oBAAkB,OAAO;EAEzB,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
+ {"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 { filePathToMcpUri, resolveMcpUri } from "../utils/uri-resolver.mjs";
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: filePathToMcpUri(filePath),
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 = await readMcpResource(uri);
104
- const filePath = resolveMcpUri(uri);
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 { readMcpResource } from '../resources/resource-reader.js';\nimport { resolveMcpUri, filePathToMcpUri } from '../utils/uri-resolver.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: filePathToMcpUri(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 = await readMcpResource(uri);\n const filePath = resolveMcpUri(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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"}
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,6 +1,6 @@
1
1
  {
2
2
  "name": "backlog-mcp",
3
- "version": "0.25.1",
3
+ "version": "0.26.0",
4
4
  "description": "Minimal task backlog MCP server for Claude and AI agents",
5
5
  "keywords": [
6
6
  "mcp",
@@ -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"}
@@ -1,7 +0,0 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
-
3
- //#region src/resources/tasks.d.ts
4
- declare function registerTasksResource(server: McpServer): void;
5
- //#endregion
6
- export { registerTasksResource };
7
- //# sourceMappingURL=tasks.d.mts.map
@@ -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"}
@@ -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
@@ -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
@@ -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"}