@hauptsache.net/clickup-mcp 1.7.0 → 1.7.3

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.
@@ -4,7 +4,8 @@ exports.isClickUpAttachmentUrl = isClickUpAttachmentUrl;
4
4
  exports.resolveImageSource = resolveImageSource;
5
5
  exports.captionToFilename = captionToFilename;
6
6
  exports.uploadTaskAttachment = uploadTaskAttachment;
7
- exports.uploadMarkdownImages = uploadMarkdownImages;
7
+ exports.resolveMarkdownImages = resolveMarkdownImages;
8
+ exports.uploadResolvedImages = uploadResolvedImages;
8
9
  exports.toAttachmentMap = toAttachmentMap;
9
10
  const buffer_1 = require("buffer");
10
11
  const crypto_1 = require("crypto");
@@ -185,31 +186,51 @@ async function uploadTaskAttachment(taskId, filename, bytes, mimeType) {
185
186
  return attachment;
186
187
  }
187
188
  /**
188
- * Upload every image referenced in the markdown to the given task.
189
+ * Phase 1 of attaching images: resolve every source without writing anything.
189
190
  *
190
- * Uploads run sequentially: a typical comment has a handful of screenshots, and
191
- * N uploads plus one write call stays well inside ClickUp's 100 calls/minute.
192
- * A failing image never fails the whole batch - the caller writes the comment
193
- * anyway and reports which images did not make it.
191
+ * Reads local files, downloads http(s) URLs, decodes data URIs and validates
192
+ * magic bytes and size. Failures are collected instead of thrown so the caller
193
+ * can report every broken reference at once - and abort before anything is
194
+ * posted to ClickUp. Identical sources are resolved once.
194
195
  */
195
- async function uploadMarkdownImages(taskId, images, baseDir = process.cwd()) {
196
- const results = [];
197
- // Identical sources are uploaded once and reused.
198
- const seen = new Map();
196
+ async function resolveMarkdownImages(images, baseDir = process.cwd()) {
197
+ const resolved = [];
198
+ const failures = [];
199
+ const seen = new Set();
199
200
  for (const { src, alt } of images) {
200
- const cached = seen.get(src);
201
- if (cached) {
202
- results.push(cached);
201
+ if (seen.has(src)) {
203
202
  continue;
204
203
  }
205
- let result;
204
+ seen.add(src);
205
+ try {
206
+ resolved.push({ src, alt, resolved: await resolveImageSource(src, baseDir) });
207
+ }
208
+ catch (error) {
209
+ const message = error instanceof Error ? error.message : "unknown error";
210
+ console.error(`Cannot use image "${src}": ${message}`);
211
+ failures.push({ src, error: message });
212
+ }
213
+ }
214
+ return { resolved, failures };
215
+ }
216
+ /**
217
+ * Phase 2: upload the resolved images to the task.
218
+ *
219
+ * Uploads run sequentially: a typical comment has a handful of screenshots, and
220
+ * N uploads plus one write call stays well inside ClickUp's 100 calls/minute.
221
+ * Stops at the first upload error - an API failure is unlikely to heal mid-batch,
222
+ * and everything uploaded so far is returned so the caller can tell a retry to
223
+ * reference those CDN URLs directly instead of uploading again.
224
+ */
225
+ async function uploadResolvedImages(taskId, images) {
226
+ const uploaded = [];
227
+ for (const { src, alt, resolved } of images) {
206
228
  try {
207
- const resolved = await resolveImageSource(src, baseDir);
208
229
  if (resolved.kind === "existing") {
209
230
  // Already on ClickUp's CDN - synthesise the minimal attachment shape so
210
231
  // the fragment builder has something to work with.
211
232
  const name = decodeURIComponent((0, path_1.basename)(new URL(resolved.url).pathname));
212
- result = {
233
+ uploaded.push({
213
234
  src,
214
235
  attachment: {
215
236
  id: name,
@@ -221,34 +242,27 @@ async function uploadMarkdownImages(taskId, images, baseDir = process.cwd()) {
221
242
  thumbnail_medium: resolved.url,
222
243
  thumbnail_large: resolved.url,
223
244
  },
224
- };
245
+ });
225
246
  }
226
247
  else {
227
248
  const filename = captionToFilename(alt, resolved.suggestedName);
228
249
  const attachment = await uploadTaskAttachment(taskId, filename, resolved.bytes, resolved.mimeType);
229
- result = { src, attachment };
250
+ uploaded.push({ src, attachment });
230
251
  }
231
252
  }
232
253
  catch (error) {
233
254
  const message = error instanceof Error ? error.message : "unknown error";
234
255
  console.error(`Failed to attach image "${src}": ${message}`);
235
- result = { src, attachment: null, error: message };
256
+ return { uploaded, failure: { src, error: message } };
236
257
  }
237
- seen.set(src, result);
238
- results.push(result);
239
258
  }
240
- return results;
259
+ return { uploaded, failure: null };
241
260
  }
242
- /**
243
- * Map from markdown `src` to the attachment that should be embedded for it.
244
- * Sources whose upload failed are absent, so the converters fall back to text.
245
- */
246
- function toAttachmentMap(results) {
261
+ /** Map from markdown `src` to the attachment that should be embedded for it. */
262
+ function toAttachmentMap(uploaded) {
247
263
  const map = new Map();
248
- for (const result of results) {
249
- if (result.attachment) {
250
- map.set(result.src, result.attachment);
251
- }
264
+ for (const { src, attachment } of uploaded) {
265
+ map.set(src, attachment);
252
266
  }
253
267
  return map;
254
268
  }
@@ -6,6 +6,7 @@ export declare const CONFIG: {
6
6
  maxImages: number;
7
7
  maxResponseSizeMB: number;
8
8
  maxUploadSizeMB: number;
9
+ commentEditWindowHours: number;
9
10
  primaryLanguageHint: string | undefined;
10
11
  mode: McpMode;
11
12
  };
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/shared/config.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,oBAA2D,CAAC;AAkDvF,MAAM,MAAM,OAAO,GAAG,cAAc,GAAG,MAAM,GAAG,OAAO,CAAC;AAUxD,eAAO,MAAM,MAAM;;;;;;;;CAWlB,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/shared/config.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,oBAA2D,CAAC;AAkDvF,MAAM,MAAM,OAAO,GAAG,cAAc,GAAG,MAAM,GAAG,OAAO,CAAC;AA8CxD,eAAO,MAAM,MAAM;;;;;;;;;CAqBlB,CAAC"}
@@ -48,6 +48,36 @@ if (rawMode === 'read-minimal' || rawMode === 'read') {
48
48
  else if (rawMode && rawMode !== 'write') {
49
49
  console.error(`Invalid CLICKUP_MCP_MODE "${rawMode}". Using default "write". Valid options: read-minimal, read, write`);
50
50
  }
51
+ /**
52
+ * Numeric settings are exposed as optional MCPB user_config fields, so their env
53
+ * values can arrive blank or - if the host does not substitute an unset optional
54
+ * field - as the literal `${user_config.x}` placeholder. Both mean "not configured"
55
+ * and must fall back to the default rather than fail or turn into NaN.
56
+ */
57
+ function readOptionalEnv(name) {
58
+ const raw = process.env[name]?.trim();
59
+ if (!raw || /^\$\{.*}$/.test(raw)) {
60
+ return undefined;
61
+ }
62
+ return raw;
63
+ }
64
+ /**
65
+ * A typo must not silently change a limit: `parseFloat("Infinity")` would disable
66
+ * the comment age check altogether and `parseFloat("abc")` would yield NaN, which
67
+ * compares false against every size and so lifts the upload limit instead of
68
+ * enforcing it. Fail at startup instead.
69
+ */
70
+ function parseNumericEnv(name, fallback, { min, expectation }) {
71
+ const raw = readOptionalEnv(name);
72
+ if (raw === undefined) {
73
+ return fallback;
74
+ }
75
+ const value = Number(raw);
76
+ if (!Number.isFinite(value) || value < min) {
77
+ throw new Error(`Invalid ${name} "${raw}". ${expectation}`);
78
+ }
79
+ return value;
80
+ }
51
81
  exports.CONFIG = {
52
82
  apiKey: process.env.CLICKUP_API_KEY,
53
83
  teamId: process.env.CLICKUP_TEAM_ID,
@@ -56,7 +86,17 @@ exports.CONFIG = {
56
86
  // Upper bound for a single image uploaded to ClickUp. Unlike maxResponseSizeMB this is
57
87
  // not about context window budget - it only guards against accidentally pushing huge
58
88
  // files into a ticket.
59
- maxUploadSizeMB: process.env.MAX_UPLOAD_SIZE_MB ? parseFloat(process.env.MAX_UPLOAD_SIZE_MB) : 10,
89
+ maxUploadSizeMB: parseNumericEnv("MAX_UPLOAD_SIZE_MB", 10, {
90
+ min: 1,
91
+ expectation: "Expected a positive number of megabytes.",
92
+ }),
93
+ // How long after creation a comment may still be edited. ClickUp has no way to tell
94
+ // "written by this MCP" apart from "written by the token owner in the UI", so this
95
+ // window is the actual guard against rewriting history. 0 disables editing entirely.
96
+ commentEditWindowHours: parseNumericEnv("CLICKUP_COMMENT_EDIT_WINDOW_HOURS", 24, {
97
+ min: 0,
98
+ expectation: "Expected a non-negative number of hours (0 disables editComment).",
99
+ }),
60
100
  primaryLanguageHint: detectedLanguageHint, // Store the cleaned code directly
61
101
  mode: mcpMode,
62
102
  };
@@ -1,7 +1,7 @@
1
1
  import Fuse from 'fuse.js';
2
2
  /**
3
3
  * Checks if a string looks like a valid ClickUp task ID
4
- * Valid task IDs are 6-9 characters long and contain only alphanumeric characters
4
+ * Valid task IDs are 6-16 characters long and contain only alphanumeric characters
5
5
  */
6
6
  export declare function isTaskId(str: string): boolean;
7
7
  /**
@@ -23,11 +23,11 @@ const fuse_js_1 = __importDefault(require("fuse.js"));
23
23
  const GLOBAL_REFRESH_INTERVAL = 60000; // 60 seconds - that is the rate limit time frame
24
24
  /**
25
25
  * Checks if a string looks like a valid ClickUp task ID
26
- * Valid task IDs are 6-9 characters long and contain only alphanumeric characters
26
+ * Valid task IDs are 6-16 characters long and contain only alphanumeric characters
27
27
  */
28
28
  function isTaskId(str) {
29
- // Task IDs are 6-9 characters long and contain only alphanumeric characters
30
- return /^[a-z0-9]{6,9}$/i.test(str);
29
+ // Task IDs are 6-16 characters long and contain only alphanumeric characters
30
+ return /^[a-z0-9]{6,16}$/i.test(str);
31
31
  }
32
32
  // Cache for current user info to avoid repeated API calls and race conditions
33
33
  let cachedUserPromise = null;
@@ -17,11 +17,11 @@ function registerTaskToolsRead(server, userData) {
17
17
  id: zod_1.z
18
18
  .string()
19
19
  .min(6)
20
- .max(9)
20
+ .max(16)
21
21
  .refine(val => (0, utils_1.isTaskId)(val), {
22
- message: "Task ID must be 6-9 alphanumeric characters only"
22
+ message: "Task ID must be 6-16 alphanumeric characters only"
23
23
  })
24
- .describe(`The 6-9 character ID of the task to get without a prefix like "#", "CU-" or "https://app.clickup.com/t/"`),
24
+ .describe(`The 6-16 character ID of the task to get without a prefix like "#", "CU-" or "https://app.clickup.com/t/"`),
25
25
  }, {
26
26
  readOnlyHint: true
27
27
  }, async ({ id }) => {
@@ -1 +1 @@
1
- {"version":3,"file":"task-write-tools.d.ts","sourceRoot":"","sources":["../../src/tools/task-write-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAgGpE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,QA+ctE"}
1
+ {"version":3,"file":"task-write-tools.d.ts","sourceRoot":"","sources":["../../src/tools/task-write-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA+IpE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,QAumBtE"}