backlog-mcp 0.27.1 → 0.28.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.
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { paths } from "../utils/paths.mjs";
3
+ import { logger } from "../utils/logger.mjs";
3
4
  import { ensureServer } from "./server-manager.mjs";
5
+ import { DEFAULT_CONFIG, Supervisor } from "./supervisor.mjs";
4
6
  import { existsSync } from "node:fs";
5
7
  import { spawn } from "node:child_process";
6
8
 
@@ -10,18 +12,51 @@ async function runBridge(port) {
10
12
  const serverUrl = `http://localhost:${port}/mcp`;
11
13
  const mcpRemotePath = paths.getBinPath("mcp-remote");
12
14
  if (!existsSync(mcpRemotePath)) {
13
- console.error("mcp-remote not found. Please run: pnpm install");
15
+ logger.error("mcp-remote not found", { path: mcpRemotePath });
14
16
  process.exit(1);
15
17
  }
16
- spawn(mcpRemotePath, [
17
- serverUrl,
18
- "--allow-http",
19
- "--transport",
20
- "http-only"
21
- ], { stdio: "inherit" }).on("exit", (code) => process.exit(code || 0));
18
+ const supervisor = new Supervisor(DEFAULT_CONFIG);
19
+ const spawnBridge = () => {
20
+ supervisor.onStart();
21
+ const bridge = spawn(mcpRemotePath, [
22
+ serverUrl,
23
+ "--allow-http",
24
+ "--transport",
25
+ "http-only"
26
+ ], { stdio: [
27
+ "inherit",
28
+ "inherit",
29
+ "pipe"
30
+ ] });
31
+ let connectionLost = false;
32
+ bridge.stderr?.on("data", (data) => {
33
+ const msg = data.toString();
34
+ process.stderr.write(msg);
35
+ if (!connectionLost && (msg.includes("ECONNREFUSED") || msg.includes("fetch failed"))) {
36
+ connectionLost = true;
37
+ logger.warn("mcp-remote lost connection, restarting");
38
+ bridge.kill();
39
+ }
40
+ });
41
+ bridge.on("exit", async (code) => {
42
+ const result = supervisor.onExit(connectionLost ? 1 : code);
43
+ if (result.action === "stop") process.exit(0);
44
+ if (result.action === "give-up") {
45
+ logger.error("mcp-remote crashed too many times", { restarts: result.restartCount });
46
+ process.exit(1);
47
+ }
48
+ logger.warn("mcp-remote restarting", {
49
+ delay: result.delay,
50
+ attempt: result.restartCount
51
+ });
52
+ await ensureServer(port).catch(() => {});
53
+ setTimeout(spawnBridge, result.delay);
54
+ });
55
+ };
56
+ spawnBridge();
22
57
  }
23
58
  runBridge(parseInt(process.env.BACKLOG_VIEWER_PORT || "3030")).catch((error) => {
24
- console.error("Bridge error:", error);
59
+ logger.error("Bridge error", { error: String(error) });
25
60
  process.exit(1);
26
61
  });
27
62
 
@@ -1 +1 @@
1
- {"version":3,"file":"bridge.mjs","names":[],"sources":["../../src/cli/bridge.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { ensureServer } from './server-manager.js';\nimport { paths } from '@/utils/paths.js';\n\nasync function runBridge(port: number): Promise<void> {\n await ensureServer(port);\n \n const serverUrl = `http://localhost:${port}/mcp`;\n const mcpRemotePath = paths.getBinPath('mcp-remote');\n \n if (!existsSync(mcpRemotePath)) {\n console.error('mcp-remote not found. Please run: pnpm install');\n process.exit(1);\n }\n \n const bridge = spawn(mcpRemotePath, [serverUrl, '--allow-http', '--transport', 'http-only'], {\n stdio: 'inherit'\n });\n \n bridge.on('exit', (code) => process.exit(code || 0));\n}\n\nconst port = parseInt(process.env.BACKLOG_VIEWER_PORT || '3030');\nrunBridge(port).catch((error) => {\n console.error('Bridge error:', error);\n process.exit(1);\n});\n"],"mappings":";;;;;;;AAOA,eAAe,UAAU,MAA6B;AACpD,OAAM,aAAa,KAAK;CAExB,MAAM,YAAY,oBAAoB,KAAK;CAC3C,MAAM,gBAAgB,MAAM,WAAW,aAAa;AAEpD,KAAI,CAAC,WAAW,cAAc,EAAE;AAC9B,UAAQ,MAAM,iDAAiD;AAC/D,UAAQ,KAAK,EAAE;;AAOjB,CAJe,MAAM,eAAe;EAAC;EAAW;EAAgB;EAAe;EAAY,EAAE,EAC3F,OAAO,WACR,CAAC,CAEK,GAAG,SAAS,SAAS,QAAQ,KAAK,QAAQ,EAAE,CAAC;;AAItD,UADa,SAAS,QAAQ,IAAI,uBAAuB,OAAO,CACjD,CAAC,OAAO,UAAU;AAC/B,SAAQ,MAAM,iBAAiB,MAAM;AACrC,SAAQ,KAAK,EAAE;EACf"}
1
+ {"version":3,"file":"bridge.mjs","names":[],"sources":["../../src/cli/bridge.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { spawn, ChildProcess } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { ensureServer } from './server-manager.js';\nimport { Supervisor, DEFAULT_CONFIG } from './supervisor.js';\nimport { paths } from '@/utils/paths.js';\nimport { logger } from '@/utils/logger.js';\n\nasync function runBridge(port: number): Promise<void> {\n await ensureServer(port);\n \n const serverUrl = `http://localhost:${port}/mcp`;\n const mcpRemotePath = paths.getBinPath('mcp-remote');\n \n if (!existsSync(mcpRemotePath)) {\n logger.error('mcp-remote not found', { path: mcpRemotePath });\n process.exit(1);\n }\n \n const supervisor = new Supervisor(DEFAULT_CONFIG);\n \n const spawnBridge = () => {\n supervisor.onStart();\n \n const bridge = spawn(mcpRemotePath, [serverUrl, '--allow-http', '--transport', 'http-only'], {\n stdio: ['inherit', 'inherit', 'pipe']\n });\n \n let connectionLost = false;\n \n bridge.stderr?.on('data', (data: Buffer) => {\n const msg = data.toString();\n process.stderr.write(msg);\n \n // mcp-remote hangs on connection errors instead of exiting - detect and kill\n if (!connectionLost && (msg.includes('ECONNREFUSED') || msg.includes('fetch failed'))) {\n connectionLost = true;\n logger.warn('mcp-remote lost connection, restarting');\n bridge.kill();\n }\n });\n \n bridge.on('exit', async (code) => {\n const result = supervisor.onExit(connectionLost ? 1 : code);\n \n if (result.action === 'stop') {\n process.exit(0);\n }\n \n if (result.action === 'give-up') {\n logger.error('mcp-remote crashed too many times', { restarts: result.restartCount });\n process.exit(1);\n }\n \n logger.warn('mcp-remote restarting', { delay: result.delay, attempt: result.restartCount });\n \n await ensureServer(port).catch(() => {});\n setTimeout(spawnBridge, result.delay);\n });\n };\n \n spawnBridge();\n}\n\nconst port = parseInt(process.env.BACKLOG_VIEWER_PORT || '3030');\nrunBridge(port).catch((error) => {\n logger.error('Bridge error', { error: String(error) });\n process.exit(1);\n});\n"],"mappings":";;;;;;;;;AASA,eAAe,UAAU,MAA6B;AACpD,OAAM,aAAa,KAAK;CAExB,MAAM,YAAY,oBAAoB,KAAK;CAC3C,MAAM,gBAAgB,MAAM,WAAW,aAAa;AAEpD,KAAI,CAAC,WAAW,cAAc,EAAE;AAC9B,SAAO,MAAM,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAC7D,UAAQ,KAAK,EAAE;;CAGjB,MAAM,aAAa,IAAI,WAAW,eAAe;CAEjD,MAAM,oBAAoB;AACxB,aAAW,SAAS;EAEpB,MAAM,SAAS,MAAM,eAAe;GAAC;GAAW;GAAgB;GAAe;GAAY,EAAE,EAC3F,OAAO;GAAC;GAAW;GAAW;GAAO,EACtC,CAAC;EAEF,IAAI,iBAAiB;AAErB,SAAO,QAAQ,GAAG,SAAS,SAAiB;GAC1C,MAAM,MAAM,KAAK,UAAU;AAC3B,WAAQ,OAAO,MAAM,IAAI;AAGzB,OAAI,CAAC,mBAAmB,IAAI,SAAS,eAAe,IAAI,IAAI,SAAS,eAAe,GAAG;AACrF,qBAAiB;AACjB,WAAO,KAAK,yCAAyC;AACrD,WAAO,MAAM;;IAEf;AAEF,SAAO,GAAG,QAAQ,OAAO,SAAS;GAChC,MAAM,SAAS,WAAW,OAAO,iBAAiB,IAAI,KAAK;AAE3D,OAAI,OAAO,WAAW,OACpB,SAAQ,KAAK,EAAE;AAGjB,OAAI,OAAO,WAAW,WAAW;AAC/B,WAAO,MAAM,qCAAqC,EAAE,UAAU,OAAO,cAAc,CAAC;AACpF,YAAQ,KAAK,EAAE;;AAGjB,UAAO,KAAK,yBAAyB;IAAE,OAAO,OAAO;IAAO,SAAS,OAAO;IAAc,CAAC;AAE3F,SAAM,aAAa,KAAK,CAAC,YAAY,GAAG;AACxC,cAAW,aAAa,OAAO,MAAM;IACrC;;AAGJ,cAAa;;AAIf,UADa,SAAS,QAAQ,IAAI,uBAAuB,OAAO,CACjD,CAAC,OAAO,UAAU;AAC/B,QAAO,MAAM,gBAAgB,EAAE,OAAO,OAAO,MAAM,EAAE,CAAC;AACtD,SAAQ,KAAK,EAAE;EACf"}
@@ -0,0 +1,38 @@
1
+ //#region src/cli/supervisor.d.ts
2
+ /**
3
+ * Supervisor manages restart logic with exponential backoff.
4
+ * Extracted for testability - no child process or I/O dependencies.
5
+ */
6
+ interface SupervisorConfig {
7
+ maxRestarts: number;
8
+ initialDelayMs: number;
9
+ maxDelayMs: number;
10
+ successThresholdMs: number;
11
+ }
12
+ declare const DEFAULT_CONFIG: SupervisorConfig;
13
+ declare class Supervisor {
14
+ private config;
15
+ private restartCount;
16
+ private delay;
17
+ private startTime;
18
+ constructor(config?: SupervisorConfig);
19
+ /** Call when process starts */
20
+ onStart(): void;
21
+ /**
22
+ * Call when process exits. Returns action to take.
23
+ * @param code - exit code (0 = normal, null = signal, other = crash)
24
+ */
25
+ onExit(code: number | null): {
26
+ action: 'stop' | 'restart' | 'give-up';
27
+ delay?: number;
28
+ restartCount?: number;
29
+ };
30
+ /** Get current state for testing/logging */
31
+ getState(): {
32
+ restartCount: number;
33
+ delay: number;
34
+ };
35
+ }
36
+ //#endregion
37
+ export { DEFAULT_CONFIG, Supervisor, SupervisorConfig };
38
+ //# sourceMappingURL=supervisor.d.mts.map
@@ -0,0 +1,54 @@
1
+ //#region src/cli/supervisor.ts
2
+ const DEFAULT_CONFIG = {
3
+ maxRestarts: 10,
4
+ initialDelayMs: 1e3,
5
+ maxDelayMs: 3e4,
6
+ successThresholdMs: 3e4
7
+ };
8
+ var Supervisor = class {
9
+ restartCount = 0;
10
+ delay;
11
+ startTime = 0;
12
+ constructor(config = DEFAULT_CONFIG) {
13
+ this.config = config;
14
+ this.delay = config.initialDelayMs;
15
+ }
16
+ /** Call when process starts */
17
+ onStart() {
18
+ this.startTime = Date.now();
19
+ }
20
+ /**
21
+ * Call when process exits. Returns action to take.
22
+ * @param code - exit code (0 = normal, null = signal, other = crash)
23
+ */
24
+ onExit(code) {
25
+ if (code === 0 || code === null) return { action: "stop" };
26
+ if (Date.now() - this.startTime > this.config.successThresholdMs) {
27
+ this.restartCount = 0;
28
+ this.delay = this.config.initialDelayMs;
29
+ }
30
+ this.restartCount++;
31
+ if (this.restartCount > this.config.maxRestarts) return {
32
+ action: "give-up",
33
+ restartCount: this.restartCount
34
+ };
35
+ const currentDelay = this.delay;
36
+ this.delay = Math.min(this.delay * 2, this.config.maxDelayMs);
37
+ return {
38
+ action: "restart",
39
+ delay: currentDelay,
40
+ restartCount: this.restartCount
41
+ };
42
+ }
43
+ /** Get current state for testing/logging */
44
+ getState() {
45
+ return {
46
+ restartCount: this.restartCount,
47
+ delay: this.delay
48
+ };
49
+ }
50
+ };
51
+
52
+ //#endregion
53
+ export { DEFAULT_CONFIG, Supervisor };
54
+ //# sourceMappingURL=supervisor.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supervisor.mjs","names":[],"sources":["../../src/cli/supervisor.ts"],"sourcesContent":["/**\n * Supervisor manages restart logic with exponential backoff.\n * Extracted for testability - no child process or I/O dependencies.\n */\nexport interface SupervisorConfig {\n maxRestarts: number;\n initialDelayMs: number;\n maxDelayMs: number;\n successThresholdMs: number;\n}\n\nexport const DEFAULT_CONFIG: SupervisorConfig = {\n maxRestarts: 10,\n initialDelayMs: 1000,\n maxDelayMs: 30000,\n successThresholdMs: 30000,\n};\n\nexport class Supervisor {\n private restartCount = 0;\n private delay: number;\n private startTime = 0;\n \n constructor(private config: SupervisorConfig = DEFAULT_CONFIG) {\n this.delay = config.initialDelayMs;\n }\n \n /** Call when process starts */\n onStart(): void {\n this.startTime = Date.now();\n }\n \n /** \n * Call when process exits. Returns action to take.\n * @param code - exit code (0 = normal, null = signal, other = crash)\n */\n onExit(code: number | null): { action: 'stop' | 'restart' | 'give-up'; delay?: number; restartCount?: number } {\n // Normal exit or signal - stop\n if (code === 0 || code === null) {\n return { action: 'stop' };\n }\n \n // Reset if ran successfully for a while\n const runDuration = Date.now() - this.startTime;\n if (runDuration > this.config.successThresholdMs) {\n this.restartCount = 0;\n this.delay = this.config.initialDelayMs;\n }\n \n this.restartCount++;\n \n if (this.restartCount > this.config.maxRestarts) {\n return { action: 'give-up', restartCount: this.restartCount };\n }\n \n const currentDelay = this.delay;\n this.delay = Math.min(this.delay * 2, this.config.maxDelayMs);\n \n return { action: 'restart', delay: currentDelay, restartCount: this.restartCount };\n }\n \n /** Get current state for testing/logging */\n getState(): { restartCount: number; delay: number } {\n return { restartCount: this.restartCount, delay: this.delay };\n }\n}\n"],"mappings":";AAWA,MAAa,iBAAmC;CAC9C,aAAa;CACb,gBAAgB;CAChB,YAAY;CACZ,oBAAoB;CACrB;AAED,IAAa,aAAb,MAAwB;CACtB,AAAQ,eAAe;CACvB,AAAQ;CACR,AAAQ,YAAY;CAEpB,YAAY,AAAQ,SAA2B,gBAAgB;EAA3C;AAClB,OAAK,QAAQ,OAAO;;;CAItB,UAAgB;AACd,OAAK,YAAY,KAAK,KAAK;;;;;;CAO7B,OAAO,MAAwG;AAE7G,MAAI,SAAS,KAAK,SAAS,KACzB,QAAO,EAAE,QAAQ,QAAQ;AAK3B,MADoB,KAAK,KAAK,GAAG,KAAK,YACpB,KAAK,OAAO,oBAAoB;AAChD,QAAK,eAAe;AACpB,QAAK,QAAQ,KAAK,OAAO;;AAG3B,OAAK;AAEL,MAAI,KAAK,eAAe,KAAK,OAAO,YAClC,QAAO;GAAE,QAAQ;GAAW,cAAc,KAAK;GAAc;EAG/D,MAAM,eAAe,KAAK;AAC1B,OAAK,QAAQ,KAAK,IAAI,KAAK,QAAQ,GAAG,KAAK,OAAO,WAAW;AAE7D,SAAO;GAAE,QAAQ;GAAW,OAAO;GAAc,cAAc,KAAK;GAAc;;;CAIpF,WAAoD;AAClD,SAAO;GAAE,cAAc,KAAK;GAAc,OAAO,KAAK;GAAO"}
@@ -1,7 +1,7 @@
1
1
  import { paths } from "../utils/paths.mjs";
2
2
  import { applyOperation } from "./operations.mjs";
3
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
- import { join } from "node:path";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
5
  import matter from "gray-matter";
6
6
  import { z } from "zod";
7
7
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -74,18 +74,19 @@ var ResourceManager = class {
74
74
  write(uri, operation) {
75
75
  try {
76
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
- }
77
+ const canCreate = [
78
+ "create",
79
+ "append",
80
+ "insert"
81
+ ].includes(operation.type);
82
+ if (!existsSync(filePath)) if (canCreate) {
83
+ mkdirSync(dirname(filePath), { recursive: true });
84
+ writeFileSync(filePath, "", "utf-8");
85
+ } else return {
86
+ success: false,
87
+ message: "File not found",
88
+ error: `Resource not found: ${uri} (${operation.type} requires existing file)`
89
+ };
89
90
  writeFileSync(filePath, applyOperation(readFileSync(filePath, "utf-8"), operation), "utf-8");
90
91
  return {
91
92
  success: true,
@@ -129,33 +130,35 @@ var ResourceManager = class {
129
130
  */
130
131
  registerWriteTool(server) {
131
132
  server.registerTool("write_resource", {
132
- description: "Write/modify resource content with operations like str_replace, append, insert",
133
+ description: `A tool for creating and editing files on the MCP server
134
+ * The \`create\` command will override the file at \`uri\` if it already exists as a file, and otherwise create a new file
135
+ * The \`append\` command will add content to the end of a file, automatically adding a newline if the file doesn't end with one. Creates the file if it doesn't exist.
136
+ Notes for using the \`str_replace\` command:
137
+ * The \`old_str\` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
138
+ * If the \`old_str\` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in \`old_str\` to make it unique
139
+ * The \`new_str\` parameter should contain the edited lines that should replace the \`old_str\``,
133
140
  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
+ uri: z.string().describe("MCP resource URI, e.g. mcp://backlog/path/to/file.md"),
142
+ operation: z.preprocess((val) => typeof val === "string" ? JSON.parse(val) : val, z.discriminatedUnion("type", [
141
143
  z.object({
142
- type: z.literal("append"),
143
- content: z.string().describe("Content to append")
144
+ type: z.literal("create"),
145
+ file_text: z.string().describe("Content of the file to be created")
144
146
  }),
145
147
  z.object({
146
- type: z.literal("prepend"),
147
- content: z.string().describe("Content to prepend")
148
+ type: z.literal("str_replace"),
149
+ old_str: z.string().describe("String in file to replace (must match exactly)"),
150
+ new_str: z.string().describe("New string to replace old_str with")
148
151
  }),
149
152
  z.object({
150
153
  type: z.literal("insert"),
151
- line: z.number().describe("Line number to insert at (0-based)"),
152
- content: z.string().describe("Content to insert")
154
+ insert_line: z.number().describe("Line number after which new_str will be inserted"),
155
+ new_str: z.string().describe("String to insert")
153
156
  }),
154
157
  z.object({
155
- type: z.literal("delete"),
156
- content: z.string().describe("Content to delete")
158
+ type: z.literal("append"),
159
+ new_str: z.string().describe("Content to append to the file")
157
160
  })
158
- ]).describe("Operation to apply")
161
+ ])).describe("Operation to apply")
159
162
  })
160
163
  }, async ({ uri, operation }) => {
161
164
  const result = this.write(uri, operation);
@@ -1 +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":";;;;;;;;;;;;;;;AAqBA,IAAa,kBAAb,MAA6B;CAC3B,YAAY,AAAiB,SAAiB;EAAjB;;;;;;;;;;CAU7B,QAAQ,KAAqB;AAC3B,MAAI,CAAC,IAAI,WAAW,SAAS,CAC3B,OAAM,IAAI,MAAM,mBAAmB,MAAM;AAI3C,MAAI,IAAI,SAAS,KAAK,CACpB,OAAM,IAAI,MAAM,+BAA+B,MAAM;EAGvD,MAAM,MAAM,IAAI,IAAI,IAAI;AAExB,MAAI,IAAI,aAAa,UACnB,OAAM,IAAI,MAAM,qBAAqB,IAAI,SAAS,sBAAsB;EAG1E,MAAM,OAAO,IAAI,SAAS,UAAU,EAAE;AAEtC,SAAO,KAAK,KAAK,SAAS,KAAK;;;;;;;;;;CAWjC,KAAK,KAA8B;EACjC,MAAM,WAAW,KAAK,QAAQ,IAAI;AAElC,MAAI,CAAC,WAAW,SAAS,EAAE;AAEzB,OAAI,4CAA4C,KAAK,IAAI,CACvD,OAAM,IAAI,MACR,uDAAuD,IAAI,MAC5D;AAEH,SAAM,IAAI,MAAM,uBAAuB,IAAI,gBAAgB,SAAS,GAAG;;EAGzE,MAAM,UAAU,aAAa,UAAU,QAAQ;EAC/C,MAAM,MAAM,SAAS,MAAM,IAAI,CAAC,KAAK,EAAE,aAAa,IAAI;EACxD,MAAM,WAAW,KAAK,YAAY,IAAI;AAGtC,MAAI,QAAQ,MAAM;GAChB,MAAM,SAAS,OAAO,QAAQ;AAC9B,UAAO;IACL,SAAS,OAAO;IAChB,aAAa,OAAO,KAAK,OAAO,KAAK,CAAC,SAAS,IAAI,OAAO,OAAO;IACjE;IACD;;AAGH,SAAO;GACL;GACA;GACD;;;;;;;;;;CAWH,MAAM,KAAa,WAA2C;AAC5D,MAAI;GACF,MAAM,WAAW,KAAK,QAAQ,IAAI;AAElC,OAAI,CAAC,WAAW,SAAS,EAAE;AAEzB,QAAI,4CAA4C,KAAK,IAAI,CACvD,QAAO;KACL,SAAS;KACT,SAAS;KACT,OAAO,iBAAiB,IAAI;KAC7B;AAEH,WAAO;KACL,SAAS;KACT,SAAS;KACT,OAAO,uBAAuB;KAC/B;;AAKH,iBAAc,UADK,eADC,aAAa,UAAU,QAAQ,EACJ,UAAU,EACrB,QAAQ;AAE5C,UAAO;IACL,SAAS;IACT,SAAS,wBAAwB,UAAU,KAAK,MAAM;IACvD;WACM,OAAO;AACd,UAAO;IACL,SAAS;IACT,SAAS;IACT,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;IAC9D;;;;;;;;;;CAWL,MAAM,UAAiC;AACrC,MAAI,CAAC,SAAS,WAAW,KAAK,QAAQ,CACpC,QAAO;AAIT,SAAO,iBADc,SAAS,UAAU,KAAK,QAAQ,SAAS,EAAE;;;;;CAOlE,iBAAiB,QAAmB;EAClC,MAAM,WAAW,IAAI,iBACnB,yBACA,EAAE,MAAM,QAAW,CACpB;AAED,SAAO,iBACL,2BACA,UACA,EAAE,aAAa,0CAA0C,EACzD,OAAO,QAAQ;GACb,MAAM,WAAW,KAAK,KAAK,IAAI,UAAU,CAAC;AAC1C,UAAO,EACL,UAAU,CAAC;IACT,KAAK,IAAI,UAAU;IACnB,UAAU,SAAS;IACnB,MAAM,SAAS;IAChB,CAAC,EACH;IAEJ;;;;;CAMH,kBAAkB,QAAmB;AACnC,SAAO,aACL,kBACA;GACE,aAAa;GACb,aAAa,EAAE,OAAO;IACpB,KAAK,EAAE,QAAQ,CAAC,SAAS,uCAAuC;IAChE,WAAW,EAAE,mBAAmB,QAAQ;KACtC,EAAE,OAAO;MACP,MAAM,EAAE,QAAQ,cAAc;MAC9B,SAAS,EAAE,QAAQ,CAAC,SAAS,6BAA6B;MAC1D,SAAS,EAAE,QAAQ,CAAC,SAAS,qBAAqB;MACnD,CAAC;KACF,EAAE,OAAO;MACP,MAAM,EAAE,QAAQ,SAAS;MACzB,SAAS,EAAE,QAAQ,CAAC,SAAS,oBAAoB;MAClD,CAAC;KACF,EAAE,OAAO;MACP,MAAM,EAAE,QAAQ,UAAU;MAC1B,SAAS,EAAE,QAAQ,CAAC,SAAS,qBAAqB;MACnD,CAAC;KACF,EAAE,OAAO;MACP,MAAM,EAAE,QAAQ,SAAS;MACzB,MAAM,EAAE,QAAQ,CAAC,SAAS,qCAAqC;MAC/D,SAAS,EAAE,QAAQ,CAAC,SAAS,oBAAoB;MAClD,CAAC;KACF,EAAE,OAAO;MACP,MAAM,EAAE,QAAQ,SAAS;MACzB,SAAS,EAAE,QAAQ,CAAC,SAAS,oBAAoB;MAClD,CAAC;KACH,CAAC,CAAC,SAAS,qBAAqB;IAClC,CAAC;GACH,EACD,OAAO,EAAE,KAAK,gBAAgB;GAC5B,MAAM,SAAS,KAAK,MAAM,KAAK,UAAU;AACzC,UAAO,EACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,KAAK,UAAU,QAAQ,MAAM,EAAE;IAAE,CAAC,EACnE;IAEJ;;CAGH,AAAQ,YAAY,KAAqB;AASvC,SARwC;GACtC,IAAI;GACJ,MAAM;GACN,IAAI;GACJ,IAAI;GACJ,KAAK;GACN,CAEc,QAAQ;;;;;;;AAQ3B,MAAa,kBAAkB,IAAI,gBAAgB,MAAM,eAAe"}
1
+ {"version":3,"file":"manager.mjs","names":[],"sources":["../../src/resources/manager.ts"],"sourcesContent":["import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport matter from 'gray-matter';\nimport { z } from 'zod';\nimport { paths } from '@/utils/paths.js';\nimport { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { Operation, WriteResourceResult } from './types.js';\nimport { applyOperation } from './operations.js';\n\nexport interface ResourceContent {\n content: string;\n frontmatter?: Record<string, any>;\n mimeType: string;\n}\n\n/**\n * ResourceManager - Single point of responsibility for MCP resource operations.\n * \n * Pure catch-all design: mcp://backlog/{+path} → {dataDir}/{path}\n * No special cases, no magic behavior.\n */\nexport class ResourceManager {\n constructor(private readonly dataDir: string) {}\n\n /**\n * Resolve MCP URI to absolute file path.\n * Pure catch-all: mcp://backlog/path/file.md → {dataDir}/path/file.md\n * \n * @param uri MCP URI (must start with mcp://backlog/)\n * @returns Absolute file path\n * @throws Error if URI is invalid or contains path traversal\n */\n resolve(uri: string): string {\n if (!uri.startsWith('mcp://')) {\n throw new Error(`Not an MCP URI: ${uri}`);\n }\n\n // Check for path traversal BEFORE URL parsing (URL normalizes ..)\n if (uri.includes('..')) {\n throw new Error(`Path traversal not allowed: ${uri}`);\n }\n\n const url = new URL(uri);\n \n if (url.hostname !== 'backlog') {\n throw new Error(`Invalid hostname: ${url.hostname}. Expected 'backlog'`);\n }\n \n const path = url.pathname.substring(1); // Remove leading /\n \n return join(this.dataDir, path);\n }\n\n /**\n * Read resource content from MCP URI.\n * Parses frontmatter for markdown files and detects MIME type.\n * \n * @param uri MCP URI\n * @returns Resource content with frontmatter and MIME type\n * @throws Error if file not found\n */\n read(uri: string): ResourceContent {\n const filePath = this.resolve(uri);\n \n if (!existsSync(filePath)) {\n // Helpful error for common mistake: extension-less task URIs\n if (/^mcp:\\/\\/backlog\\/tasks\\/(TASK|EPIC)-\\d+$/.test(uri)) {\n throw new Error(\n `Task URIs must include .md extension. Did you mean: ${uri}.md?`\n );\n }\n throw new Error(`Resource not found: ${uri} (resolved to ${filePath})`);\n }\n \n const content = readFileSync(filePath, 'utf-8');\n const ext = filePath.split('.').pop()?.toLowerCase() || 'txt';\n const mimeType = this.getMimeType(ext);\n \n // Parse frontmatter for markdown files\n if (ext === 'md') {\n const parsed = matter(content);\n return {\n content: parsed.content,\n frontmatter: Object.keys(parsed.data).length > 0 ? parsed.data : undefined,\n mimeType,\n };\n }\n \n return {\n content,\n mimeType,\n };\n }\n\n /**\n * Write/modify resource content.\n * Applies operations like str_replace, append, insert, etc.\n * \n * @param uri MCP URI\n * @param operation Operation to apply\n * @returns Result with success status and message\n */\n write(uri: string, operation: Operation): WriteResourceResult {\n try {\n const filePath = this.resolve(uri);\n const canCreate = ['create', 'append', 'insert'].includes(operation.type);\n \n if (!existsSync(filePath)) {\n if (canCreate) {\n // Auto-create file and parent directories\n mkdirSync(dirname(filePath), { recursive: true });\n writeFileSync(filePath, '', 'utf-8');\n } else {\n // str_replace/delete need existing content\n return {\n success: false,\n message: 'File not found',\n error: `Resource not found: ${uri} (${operation.type} requires existing file)`,\n };\n }\n }\n\n const fileContent = readFileSync(filePath, 'utf-8');\n const newContent = applyOperation(fileContent, operation);\n writeFileSync(filePath, newContent, 'utf-8');\n\n return {\n success: true,\n message: `Successfully applied ${operation.type} to ${uri}`,\n };\n } catch (error) {\n return {\n success: false,\n message: 'Operation failed',\n error: error instanceof Error ? error.message : String(error),\n };\n }\n }\n\n /**\n * Convert file path to MCP URI.\n * Pure mapping: {dataDir}/path/file.md → mcp://backlog/path/file.md\n * \n * @param filePath Absolute file path\n * @returns MCP URI or null if file is outside data directory\n */\n toUri(filePath: string): string | null {\n if (!filePath.startsWith(this.dataDir)) {\n return null;\n }\n \n const relativePath = filePath.substring(this.dataDir.length + 1);\n return `mcp://backlog/${relativePath}`;\n }\n\n /**\n * Register MCP resource handler (catch-all pattern).\n */\n registerResource(server: McpServer) {\n const template = new ResourceTemplate(\n 'mcp://backlog/{+path}',\n { list: undefined }\n );\n \n server.registerResource(\n 'Data Directory Resource',\n template,\n { description: 'Any file in the backlog data directory' },\n async (uri) => {\n const resource = this.read(uri.toString());\n return { \n contents: [{ \n uri: uri.toString(), \n mimeType: resource.mimeType, \n text: resource.content \n }] \n };\n }\n );\n }\n\n /**\n * Register write_resource MCP tool.\n */\n registerWriteTool(server: McpServer) {\n server.registerTool(\n 'write_resource',\n {\n description: `A tool for creating and editing files on the MCP server\n * The \\`create\\` command will override the file at \\`uri\\` if it already exists as a file, and otherwise create a new file\n * The \\`append\\` command will add content to the end of a file, automatically adding a newline if the file doesn't end with one. Creates the file if it doesn't exist.\n Notes for using the \\`str_replace\\` command:\n * The \\`old_str\\` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n * If the \\`old_str\\` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in \\`old_str\\` to make it unique\n * The \\`new_str\\` parameter should contain the edited lines that should replace the \\`old_str\\``,\n inputSchema: z.object({\n uri: z.string().describe('MCP resource URI, e.g. mcp://backlog/path/to/file.md'),\n operation: z.preprocess(\n // Workaround: MCP clients stringify object params with $ref/oneOf schemas\n // https://github.com/anthropics/claude-code/issues/18260\n (val) => typeof val === 'string' ? JSON.parse(val) : val,\n z.discriminatedUnion('type', [\n z.object({\n type: z.literal('create'),\n file_text: z.string().describe('Content of the file to be created'),\n }),\n z.object({\n type: z.literal('str_replace'),\n old_str: z.string().describe('String in file to replace (must match exactly)'),\n new_str: z.string().describe('New string to replace old_str with'),\n }),\n z.object({\n type: z.literal('insert'),\n insert_line: z.number().describe('Line number after which new_str will be inserted'),\n new_str: z.string().describe('String to insert'),\n }),\n z.object({\n type: z.literal('append'),\n new_str: z.string().describe('Content to append to the file'),\n }),\n ])).describe('Operation to apply'),\n }),\n },\n async ({ uri, operation }) => {\n const result = this.write(uri, operation);\n return {\n content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],\n };\n }\n );\n }\n\n private getMimeType(ext: string): string {\n const mimeMap: Record<string, string> = {\n md: 'text/markdown',\n json: 'application/json',\n ts: 'text/typescript',\n js: 'application/javascript',\n txt: 'text/plain',\n };\n \n return mimeMap[ext] || 'text/plain';\n }\n}\n\n/**\n * Singleton instance for dependency injection.\n * Uses the configured backlog data directory.\n */\nexport const resourceManager = new ResourceManager(paths.backlogDataDir);\n"],"mappings":"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"}
@@ -1,22 +1,22 @@
1
1
  //#region src/resources/operations.ts
2
2
  function applyOperation(content, operation) {
3
3
  switch (operation.type) {
4
+ case "create": return operation.file_text;
4
5
  case "str_replace": {
5
6
  const { old_str, new_str } = operation;
6
7
  if (!content.includes(old_str)) throw new Error(`str_replace failed: old_str not found in content`);
8
+ const firstIndex = content.indexOf(old_str);
9
+ if (content.indexOf(old_str, firstIndex + 1) !== -1) throw new Error(`str_replace failed: old_str is not unique in file. Include more context to make it unique.`);
7
10
  return content.replace(old_str, new_str);
8
11
  }
9
- case "append": return content + operation.content;
10
- case "prepend": return operation.content + content;
11
12
  case "insert": {
12
13
  const lines = content.split("\n");
13
- if (operation.line < 0 || operation.line > lines.length) throw new Error(`insert failed: line ${operation.line} out of range (0-${lines.length})`);
14
- lines.splice(operation.line, 0, operation.content);
14
+ const lineNum = operation.insert_line;
15
+ if (lineNum < 0 || lineNum > lines.length) throw new Error(`insert failed: line ${lineNum} out of range (0-${lines.length})`);
16
+ lines.splice(lineNum, 0, operation.new_str);
15
17
  return lines.join("\n");
16
18
  }
17
- case "delete":
18
- if (!content.includes(operation.content)) throw new Error(`delete failed: content not found`);
19
- return content.replace(operation.content, "");
19
+ case "append": return content + (content.length > 0 && !content.endsWith("\n") ? "\n" : "") + operation.new_str;
20
20
  default: throw new Error(`Unknown operation type: ${operation.type}`);
21
21
  }
22
22
  }
@@ -1 +1 @@
1
- {"version":3,"file":"operations.mjs","names":[],"sources":["../../src/resources/operations.ts"],"sourcesContent":["// Apply operations to text content\n\nimport type { Operation } from './types.js';\n\nexport function applyOperation(content: string, operation: Operation): string {\n switch (operation.type) {\n case 'str_replace': {\n const { old_str, new_str } = operation;\n if (!content.includes(old_str)) {\n throw new Error(`str_replace failed: old_str not found in content`);\n }\n return content.replace(old_str, new_str);\n }\n\n case 'append': {\n return content + operation.content;\n }\n\n case 'prepend': {\n return operation.content + content;\n }\n\n case 'insert': {\n const lines = content.split('\\n');\n if (operation.line < 0 || operation.line > lines.length) {\n throw new Error(`insert failed: line ${operation.line} out of range (0-${lines.length})`);\n }\n lines.splice(operation.line, 0, operation.content);\n return lines.join('\\n');\n }\n\n case 'delete': {\n if (!content.includes(operation.content)) {\n throw new Error(`delete failed: content not found`);\n }\n return content.replace(operation.content, '');\n }\n\n default:\n throw new Error(`Unknown operation type: ${(operation as any).type}`);\n }\n}\n"],"mappings":";AAIA,SAAgB,eAAe,SAAiB,WAA8B;AAC5E,SAAQ,UAAU,MAAlB;EACE,KAAK,eAAe;GAClB,MAAM,EAAE,SAAS,YAAY;AAC7B,OAAI,CAAC,QAAQ,SAAS,QAAQ,CAC5B,OAAM,IAAI,MAAM,mDAAmD;AAErE,UAAO,QAAQ,QAAQ,SAAS,QAAQ;;EAG1C,KAAK,SACH,QAAO,UAAU,UAAU;EAG7B,KAAK,UACH,QAAO,UAAU,UAAU;EAG7B,KAAK,UAAU;GACb,MAAM,QAAQ,QAAQ,MAAM,KAAK;AACjC,OAAI,UAAU,OAAO,KAAK,UAAU,OAAO,MAAM,OAC/C,OAAM,IAAI,MAAM,uBAAuB,UAAU,KAAK,mBAAmB,MAAM,OAAO,GAAG;AAE3F,SAAM,OAAO,UAAU,MAAM,GAAG,UAAU,QAAQ;AAClD,UAAO,MAAM,KAAK,KAAK;;EAGzB,KAAK;AACH,OAAI,CAAC,QAAQ,SAAS,UAAU,QAAQ,CACtC,OAAM,IAAI,MAAM,mCAAmC;AAErD,UAAO,QAAQ,QAAQ,UAAU,SAAS,GAAG;EAG/C,QACE,OAAM,IAAI,MAAM,2BAA4B,UAAkB,OAAO"}
1
+ {"version":3,"file":"operations.mjs","names":[],"sources":["../../src/resources/operations.ts"],"sourcesContent":["// Apply operations to text content (mirrors fs_write semantics)\n\nimport type { Operation } from './types.js';\n\nexport function applyOperation(content: string, operation: Operation): string {\n switch (operation.type) {\n case 'create': {\n return operation.file_text;\n }\n\n case 'str_replace': {\n const { old_str, new_str } = operation;\n if (!content.includes(old_str)) {\n throw new Error(`str_replace failed: old_str not found in content`);\n }\n // Check uniqueness - fail if old_str appears more than once\n const firstIndex = content.indexOf(old_str);\n const secondIndex = content.indexOf(old_str, firstIndex + 1);\n if (secondIndex !== -1) {\n throw new Error(`str_replace failed: old_str is not unique in file. Include more context to make it unique.`);\n }\n return content.replace(old_str, new_str);\n }\n\n case 'insert': {\n // insert_line: insert AFTER this line (1-based, like fs_write)\n const lines = content.split('\\n');\n const lineNum = operation.insert_line;\n if (lineNum < 0 || lineNum > lines.length) {\n throw new Error(`insert failed: line ${lineNum} out of range (0-${lines.length})`);\n }\n lines.splice(lineNum, 0, operation.new_str);\n return lines.join('\\n');\n }\n\n case 'append': {\n // Add newline if file doesn't end with one (like fs_write)\n const needsNewline = content.length > 0 && !content.endsWith('\\n');\n return content + (needsNewline ? '\\n' : '') + operation.new_str;\n }\n\n default:\n throw new Error(`Unknown operation type: ${(operation as any).type}`);\n }\n}\n"],"mappings":";AAIA,SAAgB,eAAe,SAAiB,WAA8B;AAC5E,SAAQ,UAAU,MAAlB;EACE,KAAK,SACH,QAAO,UAAU;EAGnB,KAAK,eAAe;GAClB,MAAM,EAAE,SAAS,YAAY;AAC7B,OAAI,CAAC,QAAQ,SAAS,QAAQ,CAC5B,OAAM,IAAI,MAAM,mDAAmD;GAGrE,MAAM,aAAa,QAAQ,QAAQ,QAAQ;AAE3C,OADoB,QAAQ,QAAQ,SAAS,aAAa,EAAE,KACxC,GAClB,OAAM,IAAI,MAAM,6FAA6F;AAE/G,UAAO,QAAQ,QAAQ,SAAS,QAAQ;;EAG1C,KAAK,UAAU;GAEb,MAAM,QAAQ,QAAQ,MAAM,KAAK;GACjC,MAAM,UAAU,UAAU;AAC1B,OAAI,UAAU,KAAK,UAAU,MAAM,OACjC,OAAM,IAAI,MAAM,uBAAuB,QAAQ,mBAAmB,MAAM,OAAO,GAAG;AAEpF,SAAM,OAAO,SAAS,GAAG,UAAU,QAAQ;AAC3C,UAAO,MAAM,KAAK,KAAK;;EAGzB,KAAK,SAGH,QAAO,WADc,QAAQ,SAAS,KAAK,CAAC,QAAQ,SAAS,KAAK,GACjC,OAAO,MAAM,UAAU;EAG1D,QACE,OAAM,IAAI,MAAM,2BAA4B,UAAkB,OAAO"}
@@ -1,33 +1,29 @@
1
1
  //#region src/resources/types.d.ts
2
- type OperationType = 'str_replace' | 'append' | 'prepend' | 'insert' | 'delete';
2
+ type OperationType = 'create' | 'str_replace' | 'insert' | 'append';
3
+ interface CreateOperation {
4
+ type: 'create';
5
+ file_text: string;
6
+ }
3
7
  interface StrReplaceOperation {
4
8
  type: 'str_replace';
5
9
  old_str: string;
6
10
  new_str: string;
7
11
  }
8
- interface AppendOperation {
9
- type: 'append';
10
- content: string;
11
- }
12
- interface PrependOperation {
13
- type: 'prepend';
14
- content: string;
15
- }
16
12
  interface InsertOperation {
17
13
  type: 'insert';
18
- line: number;
19
- content: string;
14
+ insert_line: number;
15
+ new_str: string;
20
16
  }
21
- interface DeleteOperation {
22
- type: 'delete';
23
- content: string;
17
+ interface AppendOperation {
18
+ type: 'append';
19
+ new_str: string;
24
20
  }
25
- type Operation = StrReplaceOperation | AppendOperation | PrependOperation | InsertOperation | DeleteOperation;
21
+ type Operation = CreateOperation | StrReplaceOperation | InsertOperation | AppendOperation;
26
22
  interface WriteResourceResult {
27
23
  success: boolean;
28
24
  message: string;
29
25
  error?: string;
30
26
  }
31
27
  //#endregion
32
- export { AppendOperation, DeleteOperation, InsertOperation, Operation, OperationType, PrependOperation, StrReplaceOperation, WriteResourceResult };
28
+ export { AppendOperation, CreateOperation, InsertOperation, Operation, OperationType, StrReplaceOperation, WriteResourceResult };
33
29
  //# sourceMappingURL=types.d.mts.map
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { paths } from "../utils/paths.mjs";
3
+ import { logger } from "../utils/logger.mjs";
3
4
  import { registerViewerRoutes } from "./viewer-routes.mjs";
4
5
  import { registerMcpHandler } from "./mcp-handler.mjs";
5
6
  import { authMiddleware } from "../middleware/auth.mjs";
@@ -26,12 +27,18 @@ async function startHttpServer(port = 3030) {
26
27
  port,
27
28
  host: "0.0.0.0"
28
29
  });
30
+ logger.info("Server started", {
31
+ port,
32
+ dataDir: paths.backlogDataDir,
33
+ version: paths.getVersion()
34
+ });
29
35
  console.log(`Backlog MCP server running on http://localhost:${port}`);
30
36
  console.log(`- Viewer: http://localhost:${port}/`);
31
37
  console.log(`- MCP endpoint: http://localhost:${port}/mcp`);
32
38
  console.log(`- Data directory: ${paths.backlogDataDir}`);
33
39
  }
34
40
  const shutdown = async () => {
41
+ logger.info("Server shutting down");
35
42
  console.log("Shutting down gracefully...");
36
43
  await app.close();
37
44
  setTimeout(() => process.exit(0), 500);
@@ -1 +1 @@
1
- {"version":3,"file":"fastify-server.mjs","names":[],"sources":["../../src/server/fastify-server.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport Fastify from 'fastify';\nimport cors from '@fastify/cors';\nimport { registerViewerRoutes } from './viewer-routes.js';\nimport { registerMcpHandler } from './mcp-handler.js';\nimport { authMiddleware } from '@/middleware/auth.js';\nimport { paths } from '@/utils/paths.js';\n\nconst app = Fastify({ logger: false, bodyLimit: 10 * 1024 * 1024 });\n\n// CORS\nawait app.register(cors, { origin: '*' });\n\n// Auth middleware\napp.addHook('preHandler', authMiddleware);\n\n// Register routes\nregisterViewerRoutes(app);\nregisterMcpHandler(app);\n\n// Health check\napp.get('/health', async () => ({ status: 'ok' }));\n\n// Version endpoint\napp.get('/version', async () => paths.getVersion());\n\n// Shutdown endpoint\napp.post('/shutdown', async (request, reply) => {\n reply.send('Shutting down...');\n setTimeout(() => process.exit(0), 500);\n});\n\nexport async function startHttpServer(port: number = 3030): Promise<void> {\n await app.listen({ port, host: '0.0.0.0' });\n console.log(`Backlog MCP server running on http://localhost:${port}`);\n console.log(`- Viewer: http://localhost:${port}/`);\n console.log(`- MCP endpoint: http://localhost:${port}/mcp`);\n console.log(`- Data directory: ${paths.backlogDataDir}`);\n}\n\n// Graceful shutdown\nconst shutdown = async () => {\n console.log('Shutting down gracefully...');\n await app.close();\n setTimeout(() => process.exit(0), 500);\n};\n\nprocess.on('SIGTERM', shutdown);\nprocess.on('SIGINT', shutdown);\n\n// Run if executed directly\nif (import.meta.url === `file://${process.argv[1]}`) {\n const port = parseInt(process.env.BACKLOG_VIEWER_PORT || '3030');\n startHttpServer(port).catch((error) => {\n console.error('Failed to start server:', error);\n process.exit(1);\n });\n}\n"],"mappings":";;;;;;;;;AASA,MAAM,MAAM,QAAQ;CAAE,QAAQ;CAAO,WAAW,KAAK,OAAO;CAAM,CAAC;AAGnE,MAAM,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,CAAC;AAGzC,IAAI,QAAQ,cAAc,eAAe;AAGzC,qBAAqB,IAAI;AACzB,mBAAmB,IAAI;AAGvB,IAAI,IAAI,WAAW,aAAa,EAAE,QAAQ,MAAM,EAAE;AAGlD,IAAI,IAAI,YAAY,YAAY,MAAM,YAAY,CAAC;AAGnD,IAAI,KAAK,aAAa,OAAO,SAAS,UAAU;AAC9C,OAAM,KAAK,mBAAmB;AAC9B,kBAAiB,QAAQ,KAAK,EAAE,EAAE,IAAI;EACtC;AAEF,eAAsB,gBAAgB,OAAe,MAAqB;AACxE,OAAM,IAAI,OAAO;EAAE;EAAM,MAAM;EAAW,CAAC;AAC3C,SAAQ,IAAI,kDAAkD,OAAO;AACrE,SAAQ,IAAI,8BAA8B,KAAK,GAAG;AAClD,SAAQ,IAAI,oCAAoC,KAAK,MAAM;AAC3D,SAAQ,IAAI,qBAAqB,MAAM,iBAAiB;;AAI1D,MAAM,WAAW,YAAY;AAC3B,SAAQ,IAAI,8BAA8B;AAC1C,OAAM,IAAI,OAAO;AACjB,kBAAiB,QAAQ,KAAK,EAAE,EAAE,IAAI;;AAGxC,QAAQ,GAAG,WAAW,SAAS;AAC/B,QAAQ,GAAG,UAAU,SAAS;AAG9B,IAAI,OAAO,KAAK,QAAQ,UAAU,QAAQ,KAAK,KAE7C,iBADa,SAAS,QAAQ,IAAI,uBAAuB,OAAO,CAC3C,CAAC,OAAO,UAAU;AACrC,SAAQ,MAAM,2BAA2B,MAAM;AAC/C,SAAQ,KAAK,EAAE;EACf"}
1
+ {"version":3,"file":"fastify-server.mjs","names":[],"sources":["../../src/server/fastify-server.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport Fastify from 'fastify';\nimport cors from '@fastify/cors';\nimport { registerViewerRoutes } from './viewer-routes.js';\nimport { registerMcpHandler } from './mcp-handler.js';\nimport { authMiddleware } from '@/middleware/auth.js';\nimport { paths } from '@/utils/paths.js';\nimport { logger } from '@/utils/logger.js';\n\nconst app = Fastify({ logger: false, bodyLimit: 10 * 1024 * 1024 });\n\n// CORS\nawait app.register(cors, { origin: '*' });\n\n// Auth middleware\napp.addHook('preHandler', authMiddleware);\n\n// Register routes\nregisterViewerRoutes(app);\nregisterMcpHandler(app);\n\n// Health check\napp.get('/health', async () => ({ status: 'ok' }));\n\n// Version endpoint\napp.get('/version', async () => paths.getVersion());\n\n// Shutdown endpoint\napp.post('/shutdown', async (request, reply) => {\n reply.send('Shutting down...');\n setTimeout(() => process.exit(0), 500);\n});\n\nexport async function startHttpServer(port: number = 3030): Promise<void> {\n await app.listen({ port, host: '0.0.0.0' });\n logger.info('Server started', { port, dataDir: paths.backlogDataDir, version: paths.getVersion() });\n console.log(`Backlog MCP server running on http://localhost:${port}`);\n console.log(`- Viewer: http://localhost:${port}/`);\n console.log(`- MCP endpoint: http://localhost:${port}/mcp`);\n console.log(`- Data directory: ${paths.backlogDataDir}`);\n}\n\n// Graceful shutdown\nconst shutdown = async () => {\n logger.info('Server shutting down');\n console.log('Shutting down gracefully...');\n await app.close();\n setTimeout(() => process.exit(0), 500);\n};\n\nprocess.on('SIGTERM', shutdown);\nprocess.on('SIGINT', shutdown);\n\n// Run if executed directly\nif (import.meta.url === `file://${process.argv[1]}`) {\n const port = parseInt(process.env.BACKLOG_VIEWER_PORT || '3030');\n startHttpServer(port).catch((error) => {\n console.error('Failed to start server:', error);\n process.exit(1);\n });\n}\n"],"mappings":";;;;;;;;;;AAUA,MAAM,MAAM,QAAQ;CAAE,QAAQ;CAAO,WAAW,KAAK,OAAO;CAAM,CAAC;AAGnE,MAAM,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,CAAC;AAGzC,IAAI,QAAQ,cAAc,eAAe;AAGzC,qBAAqB,IAAI;AACzB,mBAAmB,IAAI;AAGvB,IAAI,IAAI,WAAW,aAAa,EAAE,QAAQ,MAAM,EAAE;AAGlD,IAAI,IAAI,YAAY,YAAY,MAAM,YAAY,CAAC;AAGnD,IAAI,KAAK,aAAa,OAAO,SAAS,UAAU;AAC9C,OAAM,KAAK,mBAAmB;AAC9B,kBAAiB,QAAQ,KAAK,EAAE,EAAE,IAAI;EACtC;AAEF,eAAsB,gBAAgB,OAAe,MAAqB;AACxE,OAAM,IAAI,OAAO;EAAE;EAAM,MAAM;EAAW,CAAC;AAC3C,QAAO,KAAK,kBAAkB;EAAE;EAAM,SAAS,MAAM;EAAgB,SAAS,MAAM,YAAY;EAAE,CAAC;AACnG,SAAQ,IAAI,kDAAkD,OAAO;AACrE,SAAQ,IAAI,8BAA8B,KAAK,GAAG;AAClD,SAAQ,IAAI,oCAAoC,KAAK,MAAM;AAC3D,SAAQ,IAAI,qBAAqB,MAAM,iBAAiB;;AAI1D,MAAM,WAAW,YAAY;AAC3B,QAAO,KAAK,uBAAuB;AACnC,SAAQ,IAAI,8BAA8B;AAC1C,OAAM,IAAI,OAAO;AACjB,kBAAiB,QAAQ,KAAK,EAAE,EAAE,IAAI;;AAGxC,QAAQ,GAAG,WAAW,SAAS;AAC/B,QAAQ,GAAG,UAAU,SAAS;AAG9B,IAAI,OAAO,KAAK,QAAQ,UAAU,QAAQ,KAAK,KAE7C,iBADa,SAAS,QAAQ,IAAI,uBAAuB,OAAO,CAC3C,CAAC,OAAO,UAAU;AACrC,SAAQ,MAAM,2BAA2B,MAAM;AAC/C,SAAQ,KAAK,EAAE;EACf"}
@@ -1,4 +1,5 @@
1
1
  import { paths } from "../utils/paths.mjs";
2
+ import { logger } from "../utils/logger.mjs";
2
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
3
4
  import { join } from "node:path";
4
5
  import matter from "gray-matter";
@@ -43,7 +44,14 @@ var BacklogStorage = class BacklogStorage {
43
44
  if (!task.id) continue;
44
45
  yield task;
45
46
  } catch (error) {
46
- if (error.code !== "ENOENT") throw error;
47
+ if (error.code !== "ENOENT") {
48
+ const errorMessage = error instanceof Error ? error.message : String(error);
49
+ logger.warn("Malformed task file", {
50
+ file,
51
+ error: errorMessage
52
+ });
53
+ }
54
+ continue;
47
55
  }
48
56
  }
49
57
  }
@@ -1 +1 @@
1
- {"version":3,"file":"backlog.mjs","names":[],"sources":["../../src/storage/backlog.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport matter from 'gray-matter';\nimport type { Task, Status, TaskType } from './schema.js';\nimport { paths } from '@/utils/paths.js';\n\nconst TASKS_DIR = 'tasks';\n\nclass BacklogStorage {\n private static instance: BacklogStorage;\n\n static getInstance(): BacklogStorage {\n if (!BacklogStorage.instance) {\n BacklogStorage.instance = new BacklogStorage();\n }\n return BacklogStorage.instance;\n }\n\n private get tasksPath(): string {\n return join(paths.backlogDataDir, TASKS_DIR);\n }\n\n private ensureDir(dir: string): void {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n }\n\n private taskFilePath(id: string): string {\n return join(this.tasksPath, `${id}.md`);\n }\n\n private taskToMarkdown(task: Task): string {\n const { description, ...frontmatter } = task;\n return matter.stringify(description || '', frontmatter);\n }\n\n private markdownToTask(content: string): Task {\n const { data, content: description } = matter(content);\n return { ...data, description: description.trim() } as Task;\n }\n\n getFilePath(id: string): string | null {\n const path = this.taskFilePath(id);\n return existsSync(path) ? path : null;\n }\n\n private *iterateTasks(): Generator<Task> {\n if (existsSync(this.tasksPath)) {\n for (const file of readdirSync(this.tasksPath).filter(f => f.endsWith('.md'))) {\n const filePath = join(this.tasksPath, file);\n try {\n const task = this.markdownToTask(readFileSync(filePath, 'utf-8'));\n // Skip malformed tasks without valid ID\n if (!task.id) continue;\n yield task;\n } catch (error) {\n // Skip files that were deleted between listing and reading\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw error;\n }\n }\n }\n }\n }\n\n get(id: string): Task | undefined {\n const path = this.taskFilePath(id);\n if (existsSync(path)) {\n return this.markdownToTask(readFileSync(path, 'utf-8'));\n }\n return undefined;\n }\n\n getMarkdown(id: string): string | null {\n const path = this.taskFilePath(id);\n if (existsSync(path)) {\n return readFileSync(path, 'utf-8');\n }\n return null;\n }\n\n list(filter?: { status?: Status[]; type?: TaskType; epic_id?: string; limit?: number }): Task[] {\n const { status, type, epic_id, limit = 20 } = filter ?? {};\n\n let tasks = Array.from(this.iterateTasks());\n \n if (status) tasks = tasks.filter(t => status.includes(t.status));\n if (type) tasks = tasks.filter(t => (t.type ?? 'task') === type);\n if (epic_id) tasks = tasks.filter(t => t.epic_id === epic_id);\n\n return tasks\n .sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())\n .slice(0, limit);\n }\n\n add(task: Task): void {\n this.ensureDir(this.tasksPath);\n const filePath = this.taskFilePath(task.id);\n writeFileSync(filePath, this.taskToMarkdown(task));\n }\n\n save(task: Task): void {\n this.ensureDir(this.tasksPath);\n const filePath = this.taskFilePath(task.id);\n writeFileSync(filePath, this.taskToMarkdown(task));\n }\n\n delete(id: string): boolean {\n const path = this.taskFilePath(id);\n if (existsSync(path)) {\n unlinkSync(path);\n \n // Delete associated resources if they exist\n const resourcesPath = join(paths.backlogDataDir, 'resources', id);\n if (existsSync(resourcesPath)) {\n rmSync(resourcesPath, { recursive: true, force: true });\n }\n \n return true;\n }\n return false;\n }\n\n counts(): { total_tasks: number; total_epics: number; by_status: Record<Status, number> } {\n const by_status: Record<Status, number> = {\n open: 0,\n in_progress: 0,\n blocked: 0,\n done: 0,\n cancelled: 0,\n };\n\n let total_tasks = 0;\n let total_epics = 0;\n\n for (const task of this.iterateTasks()) {\n by_status[task.status]++;\n if ((task.type ?? 'task') === 'epic') {\n total_epics++;\n } else {\n total_tasks++;\n }\n }\n\n return { total_tasks, total_epics, by_status };\n }\n\n getMaxId(type?: 'task' | 'epic'): number {\n const pattern = type === 'epic' ? /^EPIC-(\\d{4,})\\.md$/ : /^TASK-(\\d{4,})\\.md$/;\n let maxNum = 0;\n\n if (existsSync(this.tasksPath)) {\n for (const file of readdirSync(this.tasksPath)) {\n const match = pattern.exec(file);\n if (match?.[1]) {\n const num = parseInt(match[1], 10);\n if (num > maxNum) maxNum = num;\n }\n }\n }\n\n return maxNum;\n }\n}\n\nexport const storage = BacklogStorage.getInstance();\n"],"mappings":"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"}
1
+ {"version":3,"file":"backlog.mjs","names":[],"sources":["../../src/storage/backlog.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport matter from 'gray-matter';\nimport type { Task, Status, TaskType } from './schema.js';\nimport { paths } from '@/utils/paths.js';\nimport { logger } from '@/utils/logger.js';\n\nconst TASKS_DIR = 'tasks';\n\nclass BacklogStorage {\n private static instance: BacklogStorage;\n\n static getInstance(): BacklogStorage {\n if (!BacklogStorage.instance) {\n BacklogStorage.instance = new BacklogStorage();\n }\n return BacklogStorage.instance;\n }\n\n private get tasksPath(): string {\n return join(paths.backlogDataDir, TASKS_DIR);\n }\n\n private ensureDir(dir: string): void {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n }\n\n private taskFilePath(id: string): string {\n return join(this.tasksPath, `${id}.md`);\n }\n\n private taskToMarkdown(task: Task): string {\n const { description, ...frontmatter } = task;\n return matter.stringify(description || '', frontmatter);\n }\n\n private markdownToTask(content: string): Task {\n const { data, content: description } = matter(content);\n return { ...data, description: description.trim() } as Task;\n }\n\n getFilePath(id: string): string | null {\n const path = this.taskFilePath(id);\n return existsSync(path) ? path : null;\n }\n\n private *iterateTasks(): Generator<Task> {\n if (existsSync(this.tasksPath)) {\n for (const file of readdirSync(this.tasksPath).filter(f => f.endsWith('.md'))) {\n const filePath = join(this.tasksPath, file);\n try {\n const task = this.markdownToTask(readFileSync(filePath, 'utf-8'));\n // Skip malformed tasks without valid ID\n if (!task.id) continue;\n yield task;\n } catch (error) {\n // Skip files that fail to parse (deleted, malformed YAML, etc.)\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n const errorMessage = error instanceof Error ? error.message : String(error);\n logger.warn('Malformed task file', { file, error: errorMessage });\n }\n continue;\n }\n }\n }\n }\n\n get(id: string): Task | undefined {\n const path = this.taskFilePath(id);\n if (existsSync(path)) {\n return this.markdownToTask(readFileSync(path, 'utf-8'));\n }\n return undefined;\n }\n\n getMarkdown(id: string): string | null {\n const path = this.taskFilePath(id);\n if (existsSync(path)) {\n return readFileSync(path, 'utf-8');\n }\n return null;\n }\n\n list(filter?: { status?: Status[]; type?: TaskType; epic_id?: string; limit?: number }): Task[] {\n const { status, type, epic_id, limit = 20 } = filter ?? {};\n\n let tasks = Array.from(this.iterateTasks());\n \n if (status) tasks = tasks.filter(t => status.includes(t.status));\n if (type) tasks = tasks.filter(t => (t.type ?? 'task') === type);\n if (epic_id) tasks = tasks.filter(t => t.epic_id === epic_id);\n\n return tasks\n .sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())\n .slice(0, limit);\n }\n\n add(task: Task): void {\n this.ensureDir(this.tasksPath);\n const filePath = this.taskFilePath(task.id);\n writeFileSync(filePath, this.taskToMarkdown(task));\n }\n\n save(task: Task): void {\n this.ensureDir(this.tasksPath);\n const filePath = this.taskFilePath(task.id);\n writeFileSync(filePath, this.taskToMarkdown(task));\n }\n\n delete(id: string): boolean {\n const path = this.taskFilePath(id);\n if (existsSync(path)) {\n unlinkSync(path);\n \n // Delete associated resources if they exist\n const resourcesPath = join(paths.backlogDataDir, 'resources', id);\n if (existsSync(resourcesPath)) {\n rmSync(resourcesPath, { recursive: true, force: true });\n }\n \n return true;\n }\n return false;\n }\n\n counts(): { total_tasks: number; total_epics: number; by_status: Record<Status, number> } {\n const by_status: Record<Status, number> = {\n open: 0,\n in_progress: 0,\n blocked: 0,\n done: 0,\n cancelled: 0,\n };\n\n let total_tasks = 0;\n let total_epics = 0;\n\n for (const task of this.iterateTasks()) {\n by_status[task.status]++;\n if ((task.type ?? 'task') === 'epic') {\n total_epics++;\n } else {\n total_tasks++;\n }\n }\n\n return { total_tasks, total_epics, by_status };\n }\n\n getMaxId(type?: 'task' | 'epic'): number {\n const pattern = type === 'epic' ? /^EPIC-(\\d{4,})\\.md$/ : /^TASK-(\\d{4,})\\.md$/;\n let maxNum = 0;\n\n if (existsSync(this.tasksPath)) {\n for (const file of readdirSync(this.tasksPath)) {\n const match = pattern.exec(file);\n if (match?.[1]) {\n const num = parseInt(match[1], 10);\n if (num > maxNum) maxNum = num;\n }\n }\n }\n\n return maxNum;\n }\n}\n\nexport const storage = BacklogStorage.getInstance();\n"],"mappings":"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"}
@@ -14,7 +14,7 @@ function registerBacklogCreateTool(server) {
14
14
  references: z.array(z.object({
15
15
  url: z.string(),
16
16
  title: z.string().optional()
17
- })).optional().describe("Reference links with optional titles")
17
+ })).optional().describe("Reference links. Formats: external URLs (https://...), task refs (mcp://backlog/tasks/TASK-XXXX.md), resources (mcp://backlog/resources/{path}). Local files must include extension (file:///path/to/file.md)")
18
18
  })
19
19
  }, async ({ title, description, type, epic_id, references }) => {
20
20
  const task = createTask({
@@ -1 +1 @@
1
- {"version":3,"file":"backlog-create.mjs","names":[],"sources":["../../src/tools/backlog-create.ts"],"sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\nimport { storage } from '../storage/backlog.js';\nimport { nextTaskId, createTask, TASK_TYPES } from '../storage/schema.js';\n\nexport function registerBacklogCreateTool(server: McpServer) {\n server.registerTool(\n 'backlog_create',\n {\n description: 'Create a new task in the backlog.',\n inputSchema: z.object({\n title: z.string().describe('Task title'),\n description: z.string().optional().describe('Task description in markdown'),\n type: z.enum(TASK_TYPES).optional().describe('Type: task (default) or epic'),\n epic_id: z.string().optional().describe('Parent epic ID to link this task to'),\n references: z.array(z.object({ url: z.string(), title: z.string().optional() })).optional().describe('Reference links with optional titles'),\n }),\n },\n async ({ title, description, type, epic_id, references }) => {\n const id = nextTaskId(storage.getMaxId(type), type);\n const task = createTask({ id, title, description, type, epic_id, references });\n storage.add(task);\n return { content: [{ type: 'text', text: `Created ${task.id}` }] };\n }\n );\n}\n"],"mappings":";;;;;AAKA,SAAgB,0BAA0B,QAAmB;AAC3D,QAAO,aACL,kBACA;EACE,aAAa;EACb,aAAa,EAAE,OAAO;GACpB,OAAO,EAAE,QAAQ,CAAC,SAAS,aAAa;GACxC,aAAa,EAAE,QAAQ,CAAC,UAAU,CAAC,SAAS,+BAA+B;GAC3E,MAAM,EAAE,KAAK,WAAW,CAAC,UAAU,CAAC,SAAS,+BAA+B;GAC5E,SAAS,EAAE,QAAQ,CAAC,UAAU,CAAC,SAAS,sCAAsC;GAC9E,YAAY,EAAE,MAAM,EAAE,OAAO;IAAE,KAAK,EAAE,QAAQ;IAAE,OAAO,EAAE,QAAQ,CAAC,UAAU;IAAE,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,uCAAuC;GAC7I,CAAC;EACH,EACD,OAAO,EAAE,OAAO,aAAa,MAAM,SAAS,iBAAiB;EAE3D,MAAM,OAAO,WAAW;GAAE,IADf,WAAW,QAAQ,SAAS,KAAK,EAAE,KAAK;GACrB;GAAO;GAAa;GAAM;GAAS;GAAY,CAAC;AAC9E,UAAQ,IAAI,KAAK;AACjB,SAAO,EAAE,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,WAAW,KAAK;GAAM,CAAC,EAAE;GAErE"}
1
+ {"version":3,"file":"backlog-create.mjs","names":[],"sources":["../../src/tools/backlog-create.ts"],"sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\nimport { storage } from '../storage/backlog.js';\nimport { nextTaskId, createTask, TASK_TYPES } from '../storage/schema.js';\n\nexport function registerBacklogCreateTool(server: McpServer) {\n server.registerTool(\n 'backlog_create',\n {\n description: 'Create a new task in the backlog.',\n inputSchema: z.object({\n title: z.string().describe('Task title'),\n description: z.string().optional().describe('Task description in markdown'),\n type: z.enum(TASK_TYPES).optional().describe('Type: task (default) or epic'),\n epic_id: z.string().optional().describe('Parent epic ID to link this task to'),\n references: z.array(z.object({ url: z.string(), title: z.string().optional() })).optional().describe('Reference links. Formats: external URLs (https://...), task refs (mcp://backlog/tasks/TASK-XXXX.md), resources (mcp://backlog/resources/{path}). Local files must include extension (file:///path/to/file.md)'),\n }),\n },\n async ({ title, description, type, epic_id, references }) => {\n const id = nextTaskId(storage.getMaxId(type), type);\n const task = createTask({ id, title, description, type, epic_id, references });\n storage.add(task);\n return { content: [{ type: 'text', text: `Created ${task.id}` }] };\n }\n );\n}\n"],"mappings":";;;;;AAKA,SAAgB,0BAA0B,QAAmB;AAC3D,QAAO,aACL,kBACA;EACE,aAAa;EACb,aAAa,EAAE,OAAO;GACpB,OAAO,EAAE,QAAQ,CAAC,SAAS,aAAa;GACxC,aAAa,EAAE,QAAQ,CAAC,UAAU,CAAC,SAAS,+BAA+B;GAC3E,MAAM,EAAE,KAAK,WAAW,CAAC,UAAU,CAAC,SAAS,+BAA+B;GAC5E,SAAS,EAAE,QAAQ,CAAC,UAAU,CAAC,SAAS,sCAAsC;GAC9E,YAAY,EAAE,MAAM,EAAE,OAAO;IAAE,KAAK,EAAE,QAAQ;IAAE,OAAO,EAAE,QAAQ,CAAC,UAAU;IAAE,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,gNAAgN;GACtT,CAAC;EACH,EACD,OAAO,EAAE,OAAO,aAAa,MAAM,SAAS,iBAAiB;EAE3D,MAAM,OAAO,WAAW;GAAE,IADf,WAAW,QAAQ,SAAS,KAAK,EAAE,KAAK;GACrB;GAAO;GAAa;GAAM;GAAS;GAAY,CAAC;AAC9E,UAAQ,IAAI,KAAK;AACjB,SAAO,EAAE,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,WAAW,KAAK;GAAM,CAAC,EAAE;GAErE"}
@@ -9,7 +9,7 @@ function registerBacklogUpdateTool(server) {
9
9
  inputSchema: z.object({
10
10
  id: z.string().describe("Task ID to update"),
11
11
  title: z.string().optional().describe("New title"),
12
- description: z.string().optional().describe("New description"),
12
+ description: z.string().optional().describe("New description (replaces entire content). For appending/editing sections, use write_resource tool instead"),
13
13
  status: z.enum(STATUSES).optional().describe("New status"),
14
14
  epic_id: z.union([z.string(), z.null()]).optional().describe("Parent epic ID (null to unlink)"),
15
15
  blocked_reason: z.array(z.string()).optional().describe("Reason if status is blocked"),
@@ -17,7 +17,7 @@ function registerBacklogUpdateTool(server) {
17
17
  references: z.array(z.object({
18
18
  url: z.string(),
19
19
  title: z.string().optional()
20
- })).optional().describe("Reference links with optional titles")
20
+ })).optional().describe("Reference links. Formats: external URLs (https://...), task refs (mcp://backlog/tasks/TASK-XXXX.md), resources (mcp://backlog/resources/{path}). Local files must include extension (file:///path/to/file.md)")
21
21
  })
22
22
  }, async ({ id, ...updates }) => {
23
23
  const task = storage.get(id);
@@ -1 +1 @@
1
- {"version":3,"file":"backlog-update.mjs","names":[],"sources":["../../src/tools/backlog-update.ts"],"sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\nimport { storage } from '../storage/backlog.js';\nimport { STATUSES } from '../storage/schema.js';\n\nexport function registerBacklogUpdateTool(server: McpServer) {\n server.registerTool(\n 'backlog_update',\n {\n description: 'Update an existing task.',\n inputSchema: z.object({\n id: z.string().describe('Task ID to update'),\n title: z.string().optional().describe('New title'),\n description: z.string().optional().describe('New description'),\n status: z.enum(STATUSES).optional().describe('New status'),\n epic_id: z.union([z.string(), z.null()]).optional().describe('Parent epic ID (null to unlink)'),\n blocked_reason: z.array(z.string()).optional().describe('Reason if status is blocked'),\n evidence: z.array(z.string()).optional().describe('Proof of completion when marking done - links to PRs, docs, or notes'),\n references: z.array(z.object({ url: z.string(), title: z.string().optional() })).optional().describe('Reference links with optional titles'),\n }),\n },\n async ({ id, ...updates }) => {\n const task = storage.get(id);\n if (!task) return { content: [{ type: 'text', text: `Task ${id} not found` }], isError: true };\n Object.assign(task, updates, { updated_at: new Date().toISOString() });\n storage.save(task);\n return { content: [{ type: 'text', text: `Updated ${id}` }] };\n }\n );\n}\n"],"mappings":";;;;;AAKA,SAAgB,0BAA0B,QAAmB;AAC3D,QAAO,aACL,kBACA;EACE,aAAa;EACb,aAAa,EAAE,OAAO;GACpB,IAAI,EAAE,QAAQ,CAAC,SAAS,oBAAoB;GAC5C,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,SAAS,YAAY;GAClD,aAAa,EAAE,QAAQ,CAAC,UAAU,CAAC,SAAS,kBAAkB;GAC9D,QAAQ,EAAE,KAAK,SAAS,CAAC,UAAU,CAAC,SAAS,aAAa;GAC1D,SAAS,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,kCAAkC;GAC/F,gBAAgB,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,UAAU,CAAC,SAAS,8BAA8B;GACtF,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,UAAU,CAAC,SAAS,uEAAuE;GACzH,YAAY,EAAE,MAAM,EAAE,OAAO;IAAE,KAAK,EAAE,QAAQ;IAAE,OAAO,EAAE,QAAQ,CAAC,UAAU;IAAE,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,uCAAuC;GAC7I,CAAC;EACH,EACD,OAAO,EAAE,IAAI,GAAG,cAAc;EAC5B,MAAM,OAAO,QAAQ,IAAI,GAAG;AAC5B,MAAI,CAAC,KAAM,QAAO;GAAE,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,QAAQ,GAAG;IAAa,CAAC;GAAE,SAAS;GAAM;AAC9F,SAAO,OAAO,MAAM,SAAS,EAAE,6BAAY,IAAI,MAAM,EAAC,aAAa,EAAE,CAAC;AACtE,UAAQ,KAAK,KAAK;AAClB,SAAO,EAAE,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,WAAW;GAAM,CAAC,EAAE;GAEhE"}
1
+ {"version":3,"file":"backlog-update.mjs","names":[],"sources":["../../src/tools/backlog-update.ts"],"sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\nimport { storage } from '../storage/backlog.js';\nimport { STATUSES } from '../storage/schema.js';\n\nexport function registerBacklogUpdateTool(server: McpServer) {\n server.registerTool(\n 'backlog_update',\n {\n description: 'Update an existing task.',\n inputSchema: z.object({\n id: z.string().describe('Task ID to update'),\n title: z.string().optional().describe('New title'),\n description: z.string().optional().describe('New description (replaces entire content). For appending/editing sections, use write_resource tool instead'),\n status: z.enum(STATUSES).optional().describe('New status'),\n epic_id: z.union([z.string(), z.null()]).optional().describe('Parent epic ID (null to unlink)'),\n blocked_reason: z.array(z.string()).optional().describe('Reason if status is blocked'),\n evidence: z.array(z.string()).optional().describe('Proof of completion when marking done - links to PRs, docs, or notes'),\n references: z.array(z.object({ url: z.string(), title: z.string().optional() })).optional().describe('Reference links. Formats: external URLs (https://...), task refs (mcp://backlog/tasks/TASK-XXXX.md), resources (mcp://backlog/resources/{path}). Local files must include extension (file:///path/to/file.md)'),\n }),\n },\n async ({ id, ...updates }) => {\n const task = storage.get(id);\n if (!task) return { content: [{ type: 'text', text: `Task ${id} not found` }], isError: true };\n Object.assign(task, updates, { updated_at: new Date().toISOString() });\n storage.save(task);\n return { content: [{ type: 'text', text: `Updated ${id}` }] };\n }\n );\n}\n"],"mappings":";;;;;AAKA,SAAgB,0BAA0B,QAAmB;AAC3D,QAAO,aACL,kBACA;EACE,aAAa;EACb,aAAa,EAAE,OAAO;GACpB,IAAI,EAAE,QAAQ,CAAC,SAAS,oBAAoB;GAC5C,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,SAAS,YAAY;GAClD,aAAa,EAAE,QAAQ,CAAC,UAAU,CAAC,SAAS,6GAA6G;GACzJ,QAAQ,EAAE,KAAK,SAAS,CAAC,UAAU,CAAC,SAAS,aAAa;GAC1D,SAAS,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,kCAAkC;GAC/F,gBAAgB,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,UAAU,CAAC,SAAS,8BAA8B;GACtF,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,UAAU,CAAC,SAAS,uEAAuE;GACzH,YAAY,EAAE,MAAM,EAAE,OAAO;IAAE,KAAK,EAAE,QAAQ;IAAE,OAAO,EAAE,QAAQ,CAAC,UAAU;IAAE,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,gNAAgN;GACtT,CAAC;EACH,EACD,OAAO,EAAE,IAAI,GAAG,cAAc;EAC5B,MAAM,OAAO,QAAQ,IAAI,GAAG;AAC5B,MAAI,CAAC,KAAM,QAAO;GAAE,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,QAAQ,GAAG;IAAa,CAAC;GAAE,SAAS;GAAM;AAC9F,SAAO,OAAO,MAAM,SAAS,EAAE,6BAAY,IAAI,MAAM,EAAC,aAAa,EAAE,CAAC;AACtE,UAAQ,KAAK,KAAK;AAClB,SAAO,EAAE,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,WAAW;GAAM,CAAC,EAAE;GAEhE"}
@@ -0,0 +1,10 @@
1
+ //#region src/utils/logger.d.ts
2
+ declare const logger: {
3
+ debug: (message: string, data?: Record<string, unknown>) => void;
4
+ info: (message: string, data?: Record<string, unknown>) => void;
5
+ warn: (message: string, data?: Record<string, unknown>) => void;
6
+ error: (message: string, data?: Record<string, unknown>) => void;
7
+ };
8
+ //#endregion
9
+ export { logger };
10
+ //# sourceMappingURL=logger.d.mts.map
@@ -0,0 +1,49 @@
1
+ import { paths } from "./paths.mjs";
2
+ import { appendFile, existsSync, mkdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ //#region src/utils/logger.ts
6
+ const LEVELS = [
7
+ "debug",
8
+ "info",
9
+ "warn",
10
+ "error"
11
+ ];
12
+ const levelPriority = {
13
+ debug: 0,
14
+ info: 1,
15
+ warn: 2,
16
+ error: 3
17
+ };
18
+ function getLogLevel() {
19
+ const env = process.env.LOG_LEVEL?.toLowerCase();
20
+ return LEVELS.includes(env) ? env : "info";
21
+ }
22
+ function getLogFile() {
23
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
24
+ return join(paths.backlogDataDir, "logs", `backlog-${date}.log`);
25
+ }
26
+ function write(level, message, data) {
27
+ if (levelPriority[level] < levelPriority[getLogLevel()]) return;
28
+ const logDir = join(paths.backlogDataDir, "logs");
29
+ if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
30
+ const entry = JSON.stringify({
31
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
32
+ level,
33
+ message,
34
+ ...data
35
+ });
36
+ appendFile(getLogFile(), entry + "\n", (err) => {
37
+ if (err) process.stderr.write(`Logger error: ${err.message}\n`);
38
+ });
39
+ }
40
+ const logger = {
41
+ debug: (message, data) => write("debug", message, data),
42
+ info: (message, data) => write("info", message, data),
43
+ warn: (message, data) => write("warn", message, data),
44
+ error: (message, data) => write("error", message, data)
45
+ };
46
+
47
+ //#endregion
48
+ export { logger };
49
+ //# sourceMappingURL=logger.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.mjs","names":[],"sources":["../../src/utils/logger.ts"],"sourcesContent":["import { appendFile, mkdirSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { paths } from './paths.js';\n\nconst LEVELS = ['debug', 'info', 'warn', 'error'] as const;\ntype Level = (typeof LEVELS)[number];\n\nconst levelPriority: Record<Level, number> = { debug: 0, info: 1, warn: 2, error: 3 };\n\nfunction getLogLevel(): Level {\n const env = process.env.LOG_LEVEL?.toLowerCase();\n return LEVELS.includes(env as Level) ? (env as Level) : 'info';\n}\n\nfunction getLogFile(): string {\n const date = new Date().toISOString().split('T')[0];\n return join(paths.backlogDataDir, 'logs', `backlog-${date}.log`);\n}\n\nfunction write(level: Level, message: string, data?: Record<string, unknown>): void {\n if (levelPriority[level] < levelPriority[getLogLevel()]) return;\n\n const logDir = join(paths.backlogDataDir, 'logs');\n if (!existsSync(logDir)) {\n mkdirSync(logDir, { recursive: true });\n }\n\n const entry = JSON.stringify({\n timestamp: new Date().toISOString(),\n level,\n message,\n ...data,\n });\n\n appendFile(getLogFile(), entry + '\\n', (err) => {\n if (err) {\n process.stderr.write(`Logger error: ${err.message}\\n`);\n }\n });\n}\n\nexport const logger = {\n debug: (message: string, data?: Record<string, unknown>) => write('debug', message, data),\n info: (message: string, data?: Record<string, unknown>) => write('info', message, data),\n warn: (message: string, data?: Record<string, unknown>) => write('warn', message, data),\n error: (message: string, data?: Record<string, unknown>) => write('error', message, data),\n};\n"],"mappings":";;;;;AAIA,MAAM,SAAS;CAAC;CAAS;CAAQ;CAAQ;CAAQ;AAGjD,MAAM,gBAAuC;CAAE,OAAO;CAAG,MAAM;CAAG,MAAM;CAAG,OAAO;CAAG;AAErF,SAAS,cAAqB;CAC5B,MAAM,MAAM,QAAQ,IAAI,WAAW,aAAa;AAChD,QAAO,OAAO,SAAS,IAAa,GAAI,MAAgB;;AAG1D,SAAS,aAAqB;CAC5B,MAAM,wBAAO,IAAI,MAAM,EAAC,aAAa,CAAC,MAAM,IAAI,CAAC;AACjD,QAAO,KAAK,MAAM,gBAAgB,QAAQ,WAAW,KAAK,MAAM;;AAGlE,SAAS,MAAM,OAAc,SAAiB,MAAsC;AAClF,KAAI,cAAc,SAAS,cAAc,aAAa,EAAG;CAEzD,MAAM,SAAS,KAAK,MAAM,gBAAgB,OAAO;AACjD,KAAI,CAAC,WAAW,OAAO,CACrB,WAAU,QAAQ,EAAE,WAAW,MAAM,CAAC;CAGxC,MAAM,QAAQ,KAAK,UAAU;EAC3B,4BAAW,IAAI,MAAM,EAAC,aAAa;EACnC;EACA;EACA,GAAG;EACJ,CAAC;AAEF,YAAW,YAAY,EAAE,QAAQ,OAAO,QAAQ;AAC9C,MAAI,IACF,SAAQ,OAAO,MAAM,iBAAiB,IAAI,QAAQ,IAAI;GAExD;;AAGJ,MAAa,SAAS;CACpB,QAAQ,SAAiB,SAAmC,MAAM,SAAS,SAAS,KAAK;CACzF,OAAO,SAAiB,SAAmC,MAAM,QAAQ,SAAS,KAAK;CACvF,OAAO,SAAiB,SAAmC,MAAM,QAAQ,SAAS,KAAK;CACvF,QAAQ,SAAiB,SAAmC,MAAM,SAAS,SAAS,KAAK;CAC1F"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backlog-mcp",
3
- "version": "0.27.1",
3
+ "version": "0.28.0",
4
4
  "description": "Minimal task backlog MCP server for Claude and AI agents",
5
5
  "keywords": [
6
6
  "mcp",