@myspec/mcp-server 1.1.1-next.163 → 1.1.1-next.165

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 (3) hide show
  1. package/README.md +3 -0
  2. package/dist/index.js +373 -42
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -363,6 +363,9 @@ Local state is stored under `~/.myspec/` (alongside the on-disk cache root), spl
363
363
  | `reply_spec_file_comment_thread` | `file_id`, `thread_id`, `body` | Replies to an existing thread. Conflicts on a trashed file |
364
364
  | `update_spec_file_comment` | `file_id`, `comment_id`, `body`, optional `expected_version` | Edits the body of one of YOUR OWN comments; the platform refuses somebody else's. `expected_version` is the **comment's** own `content_version`, not the file's — pairing it with the file would fail an edit because somebody saved the document. Works on a trashed file |
365
365
  | `delete_spec_file_comment` | `file_id`, `comment_id` | Soft-deletes one of YOUR OWN comments. Three outcomes, reported in the result: a reply disappears (`comment: null`); a first comment with live replies becomes a tombstone so the thread keeps a head (`comment` is the tombstone); a first comment with no live replies takes the whole thread (`thread_deleted: true`) |
366
+ | `list_tasks` | `project_id`, optional `status`, `assignee_user_id`, `cycle`, `label`, `file_id`, `include_details`, `limit`, `offset` | The project's task board: every checkbox task in its tasks.md files, in file order, each with a stable `task_id`, `status` (`pending`/`in_progress`/`done`), assignee, `priority` (0 none … 4 low, plus `priority_name`), `story_points`, `cycle`, `start_date`/`end_date` (calendar days, `YYYY-MM-DD`), `labels`, and with `include_details` a 500-character preview of its details. Also the files and the labels and cycles in use. Paged: 100 tasks by default (`limit` up to 500), with `total_tasks` and, while more remain, `next_offset` |
367
+ | `update_tasks` | `project_id`, `task_ids[]` (≤ 200), any of `assignee_user_id`, `priority`, `story_points`, `cycle`, `start_date`, `end_date`, `add_labels[]`, `remove_labels[]` | Edits planning fields on every task given. A field left out is left alone; `null` clears it. The assignee must be an org member who can open the project. All or nothing: an unknown id fails the call. Returns the tasks as they are now |
368
+ | `set_task_status` | `project_id`, `task_ids[]` (≤ 200), `status` | Rewrites the tasks' checkboxes in tasks.md (`[ ]`, `[-]`, `[x]`), one revision per file, recorded as an AI change. Partial: returns `updated`/`unchanged`/`removed`/`failed` counts, an outcome per task (with an error code such as `conflict` or `quota_exceeded`) and per file, and the tasks as they are now; an error result only when every task failed |
366
369
 
367
370
  There is deliberately **no tool to resolve, unresolve or re-anchor a thread**. Those three are human-only: resolving declares a review finished, and re-anchoring is the repair a person makes after the automatic pass has already failed to locate the passage. The platform refuses them for an AI caller with a 403, so a tool would only ever return an error.
368
371
 
package/dist/index.js CHANGED
@@ -2236,6 +2236,7 @@ function transformSpecFileResponse(raw) {
2236
2236
  createdAt: new Date(raw.created_at),
2237
2237
  updatedAt: new Date(raw.updated_at),
2238
2238
  trashedAt: raw.trashed_at ? new Date(raw.trashed_at) : void 0,
2239
+ trashedBy: raw.trashed_by || void 0,
2239
2240
  deletedAt: raw.deleted_at ? new Date(raw.deleted_at) : void 0
2240
2241
  };
2241
2242
  }
@@ -2277,6 +2278,8 @@ function transformProjectResponse(raw) {
2277
2278
  name: raw.name,
2278
2279
  description: raw.description,
2279
2280
  status: raw.status,
2281
+ archivedAt: raw.archived_at ? new Date(raw.archived_at) : void 0,
2282
+ archivedBy: raw.archived_by || void 0,
2280
2283
  projectType: toProjectType(raw.project_type),
2281
2284
  metadata: raw.metadata,
2282
2285
  createdAt: new Date(raw.created_at),
@@ -2304,6 +2307,7 @@ function transformSpecSessionResponse(raw) {
2304
2307
  startedAt: new Date(raw.started_at),
2305
2308
  completedAt: raw.completed_at ? new Date(raw.completed_at) : void 0,
2306
2309
  archivedAt: raw.archived_at ? new Date(raw.archived_at) : void 0,
2310
+ archivedBy: raw.archived_by || void 0,
2307
2311
  createdAt: new Date(raw.created_at),
2308
2312
  updatedAt: new Date(raw.updated_at),
2309
2313
  deletedAt: raw.deleted_at ? new Date(raw.deleted_at) : void 0,
@@ -2314,13 +2318,108 @@ function transformSpecSessionResponse(raw) {
2314
2318
  // ../../packages/platform-client/project/types/lock.types.ts
2315
2319
  var SPEC_FILE_LOCK_TTL_MS = 2 * 6e4;
2316
2320
 
2321
+ // ../../packages/platform-client/project/types/task.types.ts
2322
+ var TASK_STATUSES = ["pending", "in_progress", "done"];
2323
+ var TASK_PRIORITIES = [0, 1, 2, 3, 4];
2324
+ var TASK_LIMITS = {
2325
+ maxLabels: 20,
2326
+ maxLabelLength: 40,
2327
+ maxStoryPoints: 9999.99,
2328
+ maxTasksPerRequest: 200
2329
+ };
2330
+ function parseTimestamp(raw) {
2331
+ if (!raw) return null;
2332
+ const d = new Date(raw);
2333
+ return Number.isNaN(d.getTime()) ? null : d;
2334
+ }
2335
+ function toStatus(raw) {
2336
+ return TASK_STATUSES.includes(raw) ? raw : "pending";
2337
+ }
2338
+ function toPriority(raw) {
2339
+ return TASK_PRIORITIES.includes(raw) ? raw : 0;
2340
+ }
2341
+ function toOutcome(raw) {
2342
+ return raw === "updated" || raw === "unchanged" || raw === "removed" ? raw : "failed";
2343
+ }
2344
+ function transformProjectTask(raw) {
2345
+ return {
2346
+ id: raw.id,
2347
+ projectId: raw.project_id,
2348
+ specFileId: raw.spec_file_id,
2349
+ parentTaskId: raw.parent_task_id ?? null,
2350
+ key: raw.key,
2351
+ title: raw.title,
2352
+ section: raw.section,
2353
+ position: raw.position,
2354
+ depth: raw.depth,
2355
+ status: toStatus(raw.status),
2356
+ body: raw.body,
2357
+ removedAt: parseTimestamp(raw.removed_at),
2358
+ statusChangedAt: parseTimestamp(raw.status_changed_at),
2359
+ completedAt: parseTimestamp(raw.completed_at),
2360
+ assigneeUserId: raw.assignee_user_id ?? null,
2361
+ priority: toPriority(raw.priority),
2362
+ storyPoints: raw.story_points ?? null,
2363
+ cycle: raw.cycle ?? null,
2364
+ startAt: parseTimestamp(raw.start_at),
2365
+ endAt: parseTimestamp(raw.end_at),
2366
+ labels: raw.labels ?? [],
2367
+ updatedBy: raw.updated_by ?? null,
2368
+ createdAt: parseTimestamp(raw.created_at),
2369
+ updatedAt: parseTimestamp(raw.updated_at)
2370
+ };
2371
+ }
2372
+ function transformTaskBoard(raw) {
2373
+ return {
2374
+ projectId: raw.project_id,
2375
+ files: (raw.files ?? []).map((f) => ({
2376
+ specFileId: f.spec_file_id,
2377
+ filePath: f.file_path,
2378
+ contentVersion: f.content_version,
2379
+ stale: f.stale
2380
+ })),
2381
+ tasks: (raw.tasks ?? []).map(transformProjectTask),
2382
+ labels: raw.labels ?? [],
2383
+ cycles: raw.cycles ?? []
2384
+ };
2385
+ }
2386
+ function transformSetTaskStatusResult(raw) {
2387
+ return {
2388
+ results: (raw.results ?? []).map((r) => ({
2389
+ taskId: r.task_id,
2390
+ outcome: toOutcome(r.outcome),
2391
+ ...r.error ? { error: r.error } : {}
2392
+ })),
2393
+ files: (raw.files ?? []).map((f) => ({
2394
+ specFileId: f.spec_file_id,
2395
+ contentVersion: f.content_version,
2396
+ written: f.written,
2397
+ ...f.error ? { error: f.error } : {}
2398
+ })),
2399
+ tasks: (raw.tasks ?? []).map(transformProjectTask)
2400
+ };
2401
+ }
2402
+ function toRawUpdateTasksRequest(taskIds, patch) {
2403
+ const set = {};
2404
+ if ("assigneeUserId" in patch) set.assignee_user_id = patch.assigneeUserId ?? null;
2405
+ if ("priority" in patch) set.priority = patch.priority ?? null;
2406
+ if ("storyPoints" in patch) set.story_points = patch.storyPoints ?? null;
2407
+ if ("cycle" in patch) set.cycle = patch.cycle ?? null;
2408
+ if ("startAt" in patch) set.start_at = patch.startAt ? patch.startAt.toISOString() : null;
2409
+ if ("endAt" in patch) set.end_at = patch.endAt ? patch.endAt.toISOString() : null;
2410
+ const body = { task_ids: taskIds, set };
2411
+ if (patch.addLabels?.length) body.add_labels = patch.addLabels;
2412
+ if (patch.removeLabels?.length) body.remove_labels = patch.removeLabels;
2413
+ return body;
2414
+ }
2415
+
2317
2416
  // ../../packages/platform-client/project/types/comment.types.ts
2318
2417
  var COMMENT_THREAD_FILTER_PARAM = "status";
2319
2418
  var COMMENT_UNRESOLVED_ONLY_PARAM = "unresolved_only";
2320
2419
  var MAX_COMMENT_BODY_LENGTH = 1e4;
2321
2420
  var MAX_ANCHOR_QUOTE_LENGTH = 2e3;
2322
2421
  var MAX_ANCHOR_CONTEXT_LENGTH = 64;
2323
- function parseTimestamp(raw) {
2422
+ function parseTimestamp2(raw) {
2324
2423
  if (!raw) {
2325
2424
  return null;
2326
2425
  }
@@ -2364,9 +2463,9 @@ function transformSpecFileCommentResponse(raw) {
2364
2463
  body: raw.body,
2365
2464
  deleted: raw.deleted,
2366
2465
  contentVersion: raw.content_version,
2367
- createdAt: parseTimestamp(raw.created_at),
2368
- updatedAt: parseTimestamp(raw.updated_at),
2369
- deletedAt: parseTimestamp(raw.deleted_at)
2466
+ createdAt: parseTimestamp2(raw.created_at),
2467
+ updatedAt: parseTimestamp2(raw.updated_at),
2468
+ deletedAt: parseTimestamp2(raw.deleted_at)
2370
2469
  };
2371
2470
  }
2372
2471
  function transformSpecFileCommentThreadResponse(raw) {
@@ -2386,10 +2485,10 @@ function transformSpecFileCommentThreadResponse(raw) {
2386
2485
  anchorStatus: toAnchorStatus(raw.anchor_status),
2387
2486
  createdBy: raw.created_by,
2388
2487
  createdByKind: toCommentActorKind(raw.created_by_kind),
2389
- resolvedAt: parseTimestamp(raw.resolved_at),
2488
+ resolvedAt: parseTimestamp2(raw.resolved_at),
2390
2489
  resolvedBy: raw.resolved_by ?? null,
2391
- createdAt: parseTimestamp(raw.created_at),
2392
- updatedAt: parseTimestamp(raw.updated_at),
2490
+ createdAt: parseTimestamp2(raw.created_at),
2491
+ updatedAt: parseTimestamp2(raw.updated_at),
2393
2492
  // Always an array: consumers map over it on every render, so the empty case
2394
2493
  // must not be a value each one has to guard.
2395
2494
  comments: (raw.comments ?? []).map(transformSpecFileCommentResponse)
@@ -2870,6 +2969,7 @@ var HttpProjectClient = class {
2870
2969
  if (opts?.limit) queryParams.set("limit", String(opts.limit));
2871
2970
  if (opts?.offset !== void 0) queryParams.set("offset", String(opts.offset));
2872
2971
  if (opts?.query) queryParams.set("q", opts.query);
2972
+ if (opts?.owner) queryParams.set("owner", opts.owner);
2873
2973
  const query = queryParams.toString();
2874
2974
  const path4 = query ? `/project/v1/projects?${query}` : "/project/v1/projects";
2875
2975
  const response = await this.httpClient.get(path4, jwtToken, {});
@@ -3147,6 +3247,40 @@ var HttpSpecFileCommentClient = class {
3147
3247
  }
3148
3248
  };
3149
3249
 
3250
+ // ../../packages/platform-client/project/http/task.http-client.ts
3251
+ var HEADER_ACTOR_KIND3 = "X-Actor-Kind";
3252
+ function tasksPath(projectId) {
3253
+ return `/project/v1/projects/${encodeURIComponent(projectId)}/tasks`;
3254
+ }
3255
+ var HttpProjectTaskClient = class {
3256
+ httpClient;
3257
+ constructor(httpClient) {
3258
+ this.httpClient = httpClient;
3259
+ }
3260
+ async listProjectTasks(projectId, jwtToken) {
3261
+ const raw = await this.httpClient.get(tasksPath(projectId), jwtToken, {});
3262
+ return transformTaskBoard(raw);
3263
+ }
3264
+ async updateProjectTasks(projectId, taskIds, patch, jwtToken) {
3265
+ const raw = await this.httpClient.patch(
3266
+ tasksPath(projectId),
3267
+ toRawUpdateTasksRequest(taskIds, patch),
3268
+ jwtToken,
3269
+ {}
3270
+ );
3271
+ return (raw.tasks ?? []).map(transformProjectTask);
3272
+ }
3273
+ async setProjectTaskStatus(projectId, taskIds, status, jwtToken, opts) {
3274
+ const raw = await this.httpClient.post(
3275
+ `${tasksPath(projectId)}/status`,
3276
+ { task_ids: taskIds, status },
3277
+ jwtToken,
3278
+ opts?.actorKind ? { headers: { [HEADER_ACTOR_KIND3]: opts.actorKind } } : {}
3279
+ );
3280
+ return transformSetTaskStatusResult(raw);
3281
+ }
3282
+ };
3283
+
3150
3284
  // ../../packages/platform-client/artifact/types/artifact.types.ts
3151
3285
  function transformArtifactResponse(raw) {
3152
3286
  return {
@@ -3278,14 +3412,14 @@ function buildUpdateArtifactBody(request) {
3278
3412
  // ../../packages/platform-client/artifact/http/artifact.http-client.ts
3279
3413
  var BASE_PATH = "/artifact/v1";
3280
3414
  var HEADER_EXPECTED_VERSION3 = "X-Expected-Version";
3281
- var HEADER_ACTOR_KIND3 = "X-Actor-Kind";
3415
+ var HEADER_ACTOR_KIND4 = "X-Actor-Kind";
3282
3416
  function writeHeaders(opts) {
3283
3417
  const headers = {};
3284
3418
  if (opts?.expectedVersion !== void 0) {
3285
3419
  headers[HEADER_EXPECTED_VERSION3] = String(opts.expectedVersion);
3286
3420
  }
3287
3421
  if (opts?.actorKind) {
3288
- headers[HEADER_ACTOR_KIND3] = opts.actorKind;
3422
+ headers[HEADER_ACTOR_KIND4] = opts.actorKind;
3289
3423
  }
3290
3424
  return headers;
3291
3425
  }
@@ -3863,7 +3997,8 @@ var PlatformClient = class {
3863
3997
  attachment: new HttpAttachmentClient(httpClient),
3864
3998
  artifact: new HttpArtifactClient(httpClient),
3865
3999
  streamToken: new HttpStreamTokenClient(httpClient),
3866
- comment: new HttpSpecFileCommentClient(httpClient)
4000
+ comment: new HttpSpecFileCommentClient(httpClient),
4001
+ task: new HttpProjectTaskClient(httpClient)
3867
4002
  };
3868
4003
  }
3869
4004
  // ------------------------------------------------------- stream tokens
@@ -3967,6 +4102,26 @@ var PlatformClient = class {
3967
4102
  (jwt, c) => c.comment.deleteComment(fileId, commentId, jwt, { actorKind: "ai" })
3968
4103
  );
3969
4104
  }
4105
+ // ---------------------------------------------------------------- tasks
4106
+ //
4107
+ // The project task board. A status change rewrites tasks.md, so it carries
4108
+ // `actorKind: 'ai'` like every other file write here: the revision is then
4109
+ // recorded as the assistant's, not filed under the user's name. A planning
4110
+ // edit writes no file and the platform records no actor kind for it.
4111
+ /** Every live task of a project's tasks.md files. */
4112
+ async listProjectTasks(projectId) {
4113
+ return this.withTokenRetry((jwt, c) => c.task.listProjectTasks(projectId, jwt));
4114
+ }
4115
+ /** Applies one planning patch to up to 200 tasks; all or nothing. */
4116
+ async updateProjectTasks(projectId, taskIds, patch) {
4117
+ return this.withTokenRetry((jwt, c) => c.task.updateProjectTasks(projectId, taskIds, patch, jwt));
4118
+ }
4119
+ /** Sets the status of up to 200 tasks by rewriting their checkboxes. */
4120
+ async setProjectTaskStatus(projectId, taskIds, status) {
4121
+ return this.withTokenRetry(
4122
+ (jwt, c) => c.task.setProjectTaskStatus(projectId, taskIds, status, jwt, { actorKind: "ai" })
4123
+ );
4124
+ }
3970
4125
  async withTokenRetry(call) {
3971
4126
  const clients = await this.clients();
3972
4127
  const jwt = await this.tokenManager.getValidAccessToken();
@@ -7175,6 +7330,179 @@ var startSpecSessionTool = {
7175
7330
  }
7176
7331
  };
7177
7332
 
7333
+ // src/server/tools/tasks.ts
7334
+ import { z as z39 } from "zod";
7335
+ var LIST_DEFAULT_TASKS = 100;
7336
+ var LIST_MAX_TASKS = 500;
7337
+ var DETAILS_PREVIEW_CHARS = 500;
7338
+ var MAX_CYCLE = 1e5;
7339
+ var PRIORITY_NAMES = ["none", "urgent", "high", "medium", "low"];
7340
+ var STATUS = z39.enum(["pending", "in_progress", "done"]);
7341
+ function toDay(date) {
7342
+ return date ? date.toISOString().slice(0, 10) : null;
7343
+ }
7344
+ function fromDay(day) {
7345
+ return /* @__PURE__ */ new Date(`${day}T00:00:00Z`);
7346
+ }
7347
+ function taskSummary(task, opts = {}) {
7348
+ const withDetails = opts.details === true && task.body !== "";
7349
+ const truncated = withDetails && task.body.length > DETAILS_PREVIEW_CHARS;
7350
+ return {
7351
+ task_id: task.id,
7352
+ spec_file_id: task.specFileId,
7353
+ // The id written in the file ("1", "1.1", "T001"); '' when it has none.
7354
+ key: task.key,
7355
+ title: task.title,
7356
+ section: task.section,
7357
+ parent_task_id: task.parentTaskId,
7358
+ status: task.status,
7359
+ assignee_user_id: task.assigneeUserId,
7360
+ priority: task.priority,
7361
+ priority_name: PRIORITY_NAMES[task.priority],
7362
+ story_points: task.storyPoints,
7363
+ cycle: task.cycle,
7364
+ start_date: toDay(task.startAt),
7365
+ end_date: toDay(task.endAt),
7366
+ labels: task.labels,
7367
+ ...withDetails ? { details: truncated ? task.body.slice(0, DETAILS_PREVIEW_CHARS) : task.body } : {},
7368
+ // Only ever present and true.
7369
+ ...truncated ? { details_truncated: true } : {},
7370
+ updated_at: task.updatedAt?.toISOString()
7371
+ };
7372
+ }
7373
+ var userIdField = z39.string().trim().min(1);
7374
+ var labelField = z39.string().trim().min(1).refine((label) => Array.from(label).length <= TASK_LIMITS.maxLabelLength, {
7375
+ message: `A label is at most ${String(TASK_LIMITS.maxLabelLength)} characters.`
7376
+ });
7377
+ function taskIdsField(what) {
7378
+ return z39.array(z39.uuid()).min(1).max(TASK_LIMITS.maxTasksPerRequest).describe(`${what} At most ${String(TASK_LIMITS.maxTasksPerRequest)} per call.`);
7379
+ }
7380
+ var listInputSchema2 = {
7381
+ project_id: z39.uuid().describe("UUID of the project whose tasks to list."),
7382
+ status: STATUS.optional().describe("Only tasks with this status."),
7383
+ assignee_user_id: userIdField.optional().describe("Only tasks assigned to this user id (as returned in assignee_user_id)."),
7384
+ cycle: z39.number().int().min(1).optional().describe("Only tasks in this cycle (sprint) number."),
7385
+ label: labelField.optional().describe("Only tasks carrying this label."),
7386
+ file_id: z39.uuid().optional().describe("Only tasks from this tasks.md (its spec file id)."),
7387
+ include_details: z39.boolean().optional().describe(
7388
+ "Also return each task's details (its sub-bullets in tasks.md), previewed to 500 characters. Off by default: they make the result several times larger."
7389
+ ),
7390
+ limit: z39.number().int().min(1).max(LIST_MAX_TASKS).optional().describe(
7391
+ `Tasks per page (default ${String(LIST_DEFAULT_TASKS)}, at most ${String(LIST_MAX_TASKS)}).`
7392
+ ),
7393
+ offset: z39.number().int().min(0).optional().describe("How many matching tasks to skip; pass next_offset from the previous page.")
7394
+ };
7395
+ var listTasksTool = {
7396
+ name: "list_tasks",
7397
+ description: "List a project's tasks: every checkbox task in its tasks.md files, in file order, each with a stable task_id, status (pending, in_progress, done), assignee, priority (0 none, 1 urgent, 2 high, 3 medium, 4 low), story points, cycle, start/end dates (YYYY-MM-DD) and labels. Also returns the files, and the labels and cycles in use. Filter by status, assignee, cycle, label or file. Paged: 100 tasks by default (limit up to 500), with total_tasks and, while more remain, next_offset. Details (sub-bullets) only with include_details. Use the task_id values with update_tasks and set_task_status.",
7398
+ inputSchema: listInputSchema2,
7399
+ handler: async (args, ctx) => {
7400
+ const board = await ctx.client.listProjectTasks(args.project_id);
7401
+ const matching = board.tasks.filter(
7402
+ (task) => (args.status === void 0 || task.status === args.status) && (args.assignee_user_id === void 0 || task.assigneeUserId === args.assignee_user_id) && (args.cycle === void 0 || task.cycle === args.cycle) && (args.label === void 0 || task.labels.includes(args.label)) && (args.file_id === void 0 || task.specFileId === args.file_id)
7403
+ );
7404
+ const offset = args.offset ?? 0;
7405
+ const page = matching.slice(offset, offset + (args.limit ?? LIST_DEFAULT_TASKS));
7406
+ const next = offset + page.length;
7407
+ return jsonResult({
7408
+ project_id: board.projectId,
7409
+ files: board.files.map((file) => ({
7410
+ spec_file_id: file.specFileId,
7411
+ file_path: file.filePath,
7412
+ content_version: file.contentVersion,
7413
+ // The file could not be re-read just now: its tasks are from the last read.
7414
+ ...file.stale ? { stale: true } : {}
7415
+ })),
7416
+ tasks: page.map((task) => taskSummary(task, { details: args.include_details })),
7417
+ total_tasks: matching.length,
7418
+ offset,
7419
+ // Only while more remain, so its absence means "that was the last page".
7420
+ ...next < matching.length ? { next_offset: next } : {},
7421
+ labels: board.labels,
7422
+ cycles: board.cycles
7423
+ });
7424
+ }
7425
+ };
7426
+ var updateInputSchema2 = {
7427
+ project_id: z39.uuid().describe("UUID of the project the tasks belong to."),
7428
+ task_ids: taskIdsField("The tasks to change (task_id values from list_tasks)."),
7429
+ assignee_user_id: userIdField.nullable().optional().describe(
7430
+ "User id to assign, or null to unassign. The person must be a member of the organisation who can open the project."
7431
+ ),
7432
+ priority: z39.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low."),
7433
+ story_points: z39.number().min(0).max(TASK_LIMITS.maxStoryPoints).nullable().optional().describe("Estimate in story points, or null to clear it."),
7434
+ cycle: z39.number().int().min(1).max(MAX_CYCLE).nullable().optional().describe("Cycle (sprint) number, or null to clear it."),
7435
+ start_date: z39.iso.date().nullable().optional().describe("Start day as YYYY-MM-DD, or null to clear it."),
7436
+ end_date: z39.iso.date().nullable().optional().describe("Due day as YYYY-MM-DD, or null to clear it. Not before the start day."),
7437
+ add_labels: z39.array(labelField).max(TASK_LIMITS.maxLabels).optional().describe(`Labels to add. A task carries at most ${String(TASK_LIMITS.maxLabels)}.`),
7438
+ remove_labels: z39.array(labelField).max(TASK_LIMITS.maxLabels).optional().describe("Labels to remove.")
7439
+ };
7440
+ var updateTasksTool = {
7441
+ name: "update_tasks",
7442
+ description: "Edit the planning fields of one or more tasks: assignee, priority, story points, cycle, start and due dates, and labels. The same change applies to every task given. A field left out is left alone; null clears it. Labels are added and removed, not replaced. Does not change status or the task's text, which live in tasks.md: use set_task_status for status. All or nothing: an unknown task id fails the whole call. Returns the tasks as they are now.",
7443
+ inputSchema: updateInputSchema2,
7444
+ handler: async (args, ctx) => {
7445
+ const patch = {};
7446
+ if (args.assignee_user_id !== void 0) patch.assigneeUserId = args.assignee_user_id;
7447
+ if (args.priority !== void 0) patch.priority = args.priority;
7448
+ if (args.story_points !== void 0) patch.storyPoints = args.story_points;
7449
+ if (args.cycle !== void 0) patch.cycle = args.cycle;
7450
+ if (args.start_date !== void 0) {
7451
+ patch.startAt = args.start_date === null ? null : fromDay(args.start_date);
7452
+ }
7453
+ if (args.end_date !== void 0) {
7454
+ patch.endAt = args.end_date === null ? null : fromDay(args.end_date);
7455
+ }
7456
+ if (args.add_labels?.length) patch.addLabels = args.add_labels;
7457
+ if (args.remove_labels?.length) patch.removeLabels = args.remove_labels;
7458
+ if (Object.keys(patch).length === 0) {
7459
+ return errorResult(
7460
+ "Nothing to change: pass at least one of assignee_user_id, priority, story_points, cycle, start_date, end_date, add_labels or remove_labels."
7461
+ );
7462
+ }
7463
+ const tasks = await ctx.client.updateProjectTasks(args.project_id, args.task_ids, patch);
7464
+ return jsonResult({ tasks: tasks.map((task) => taskSummary(task)) });
7465
+ }
7466
+ };
7467
+ var statusInputSchema = {
7468
+ project_id: z39.uuid().describe("UUID of the project the tasks belong to."),
7469
+ task_ids: taskIdsField("The tasks to move (task_id values from list_tasks)."),
7470
+ status: STATUS.describe("The new status: pending, in_progress or done.")
7471
+ };
7472
+ var setTaskStatusTool = {
7473
+ name: "set_task_status",
7474
+ description: "Change the status of one or more tasks by rewriting their checkbox in the project's tasks.md files: `[ ]` pending, `[-]` in progress, `[x]` done. Each affected file gets one new revision, recorded as an AI change; nothing else in the file changes. Partial by nature: a file that cannot be written (it kept changing, the organisation is out of storage, or it was moved to the trash) fails only its own tasks. Returns an outcome per task (updated, unchanged, removed: no longer in its file, failed: with an error code), an outcome per file, and the tasks as they are now. Marked as an error only when every task failed.",
7475
+ inputSchema: statusInputSchema,
7476
+ handler: async (args, ctx) => {
7477
+ const result = await ctx.client.setProjectTaskStatus(
7478
+ args.project_id,
7479
+ args.task_ids,
7480
+ args.status
7481
+ );
7482
+ const count = (outcome) => result.results.filter((r) => r.outcome === outcome).length;
7483
+ const failed = count("failed");
7484
+ const body = jsonResult({
7485
+ updated: count("updated"),
7486
+ unchanged: count("unchanged"),
7487
+ removed: count("removed"),
7488
+ failed,
7489
+ results: result.results.map((r) => ({
7490
+ task_id: r.taskId,
7491
+ outcome: r.outcome,
7492
+ ...r.error ? { error: r.error } : {}
7493
+ })),
7494
+ files: result.files.map((file) => ({
7495
+ spec_file_id: file.specFileId,
7496
+ content_version: file.contentVersion,
7497
+ written: file.written,
7498
+ ...file.error ? { error: file.error } : {}
7499
+ })),
7500
+ tasks: result.tasks.map((task) => taskSummary(task))
7501
+ });
7502
+ return failed > 0 && failed === result.results.length ? { ...body, isError: true } : body;
7503
+ }
7504
+ };
7505
+
7178
7506
  // src/server/server.ts
7179
7507
  function buildServer(deps) {
7180
7508
  const server = new McpServer({
@@ -7226,6 +7554,9 @@ function buildServer(deps) {
7226
7554
  registerTool(server, replySpecFileCommentThreadTool, ctx);
7227
7555
  registerTool(server, updateSpecFileCommentTool, ctx);
7228
7556
  registerTool(server, deleteSpecFileCommentTool, ctx);
7557
+ registerTool(server, listTasksTool, ctx);
7558
+ registerTool(server, updateTasksTool, ctx);
7559
+ registerTool(server, setTaskStatusTool, ctx);
7229
7560
  return server;
7230
7561
  }
7231
7562
  async function startStdioServer(deps) {
@@ -7397,7 +7728,7 @@ async function canonicaliseRoot(root) {
7397
7728
  import { promises as fs4 } from "fs";
7398
7729
  import { join as join2, relative } from "path";
7399
7730
  import { spawn } from "child_process";
7400
- import { z as z39 } from "zod";
7731
+ import { z as z40 } from "zod";
7401
7732
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
7402
7733
 
7403
7734
  // src/reverse/ignore.ts
@@ -7702,37 +8033,37 @@ var READ_FILE_MAX_LINES = 2e3;
7702
8033
  var LIST_DIR_HARD_CAP = 5e3;
7703
8034
  var LIST_DIR_DEFAULT_CAP = 1e3;
7704
8035
  var GREP_MAX_MATCHES_DEFAULT = 200;
7705
- var listDirInput = z39.object({
7706
- path: z39.string(),
7707
- recursive: z39.boolean().optional(),
7708
- maxEntries: z39.number().int().positive().optional(),
7709
- includeIgnored: z39.boolean().optional()
8036
+ var listDirInput = z40.object({
8037
+ path: z40.string(),
8038
+ recursive: z40.boolean().optional(),
8039
+ maxEntries: z40.number().int().positive().optional(),
8040
+ includeIgnored: z40.boolean().optional()
7710
8041
  });
7711
- var readFileInput = z39.object({
7712
- path: z39.string(),
7713
- offset: z39.number().int().nonnegative().optional(),
7714
- limit: z39.number().int().positive().optional(),
7715
- includeIgnored: z39.boolean().optional()
8042
+ var readFileInput = z40.object({
8043
+ path: z40.string(),
8044
+ offset: z40.number().int().nonnegative().optional(),
8045
+ limit: z40.number().int().positive().optional(),
8046
+ includeIgnored: z40.boolean().optional()
7716
8047
  });
7717
- var grepInput = z39.object({
7718
- pattern: z39.string().describe(
8048
+ var grepInput = z40.object({
8049
+ pattern: z40.string().describe(
7719
8050
  "Search pattern. Interpreted as a regular expression by default (ripgrep syntax), e.g. `foo|bar` or `[Rr]ole`. Set `isRegex: false` to match the pattern as an exact literal string instead."
7720
8051
  ),
7721
- path: z39.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
7722
- isRegex: z39.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
7723
- caseSensitive: z39.boolean().optional(),
7724
- maxMatches: z39.number().int().positive().optional(),
7725
- contextLines: z39.number().int().nonnegative().optional(),
7726
- includeIgnored: z39.boolean().optional()
8052
+ path: z40.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
8053
+ isRegex: z40.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
8054
+ caseSensitive: z40.boolean().optional(),
8055
+ maxMatches: z40.number().int().positive().optional(),
8056
+ contextLines: z40.number().int().nonnegative().optional(),
8057
+ includeIgnored: z40.boolean().optional()
7727
8058
  });
7728
- var packCodebaseInput = z39.object({
7729
- subpath: z39.string().optional(),
7730
- includePatterns: z39.string().optional(),
7731
- ignorePatterns: z39.string().optional()
8059
+ var packCodebaseInput = z40.object({
8060
+ subpath: z40.string().optional(),
8061
+ includePatterns: z40.string().optional(),
8062
+ ignorePatterns: z40.string().optional()
7732
8063
  });
7733
- var packCodebaseReadPageInput = z39.object({
7734
- outputId: z39.string(),
7735
- page: z39.number().int().positive()
8064
+ var packCodebaseReadPageInput = z40.object({
8065
+ outputId: z40.string(),
8066
+ page: z40.number().int().positive()
7736
8067
  });
7737
8068
  function structuredError(error, message, extra) {
7738
8069
  const payload = { error, message, ...extra ?? {} };
@@ -8260,7 +8591,7 @@ function jsonSchemaFromZod(schema) {
8260
8591
  return zodToJson(schema);
8261
8592
  }
8262
8593
  function zodToJson(schema) {
8263
- if (schema instanceof z39.ZodObject) {
8594
+ if (schema instanceof z40.ZodObject) {
8264
8595
  const shape = schema.shape;
8265
8596
  const properties = {};
8266
8597
  const required = [];
@@ -8275,19 +8606,19 @@ function zodToJson(schema) {
8275
8606
  if (required.length > 0) out.required = required;
8276
8607
  return out;
8277
8608
  }
8278
- if (schema instanceof z39.ZodOptional) {
8609
+ if (schema instanceof z40.ZodOptional) {
8279
8610
  return zodToJson(schema.unwrap());
8280
8611
  }
8281
- if (schema instanceof z39.ZodString) {
8612
+ if (schema instanceof z40.ZodString) {
8282
8613
  return { type: "string" };
8283
8614
  }
8284
- if (schema instanceof z39.ZodNumber) {
8615
+ if (schema instanceof z40.ZodNumber) {
8285
8616
  return { type: "number" };
8286
8617
  }
8287
- if (schema instanceof z39.ZodBoolean) {
8618
+ if (schema instanceof z40.ZodBoolean) {
8288
8619
  return { type: "boolean" };
8289
8620
  }
8290
- if (schema instanceof z39.ZodEnum) {
8621
+ if (schema instanceof z40.ZodEnum) {
8291
8622
  return { type: "string", enum: schema.options };
8292
8623
  }
8293
8624
  return { type: "string" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myspec/mcp-server",
3
- "version": "1.1.1-next.163",
3
+ "version": "1.1.1-next.165",
4
4
  "description": "MySpec MCP server — exposes MySpec platform projects, files and attachments to MCP-aware clients via OAuth-authenticated access tokens.",
5
5
  "type": "module",
6
6
  "repository": {