@myspec/mcp-server 1.1.1-next.164 → 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 +368 -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
@@ -2318,13 +2318,108 @@ function transformSpecSessionResponse(raw) {
2318
2318
  // ../../packages/platform-client/project/types/lock.types.ts
2319
2319
  var SPEC_FILE_LOCK_TTL_MS = 2 * 6e4;
2320
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
+
2321
2416
  // ../../packages/platform-client/project/types/comment.types.ts
2322
2417
  var COMMENT_THREAD_FILTER_PARAM = "status";
2323
2418
  var COMMENT_UNRESOLVED_ONLY_PARAM = "unresolved_only";
2324
2419
  var MAX_COMMENT_BODY_LENGTH = 1e4;
2325
2420
  var MAX_ANCHOR_QUOTE_LENGTH = 2e3;
2326
2421
  var MAX_ANCHOR_CONTEXT_LENGTH = 64;
2327
- function parseTimestamp(raw) {
2422
+ function parseTimestamp2(raw) {
2328
2423
  if (!raw) {
2329
2424
  return null;
2330
2425
  }
@@ -2368,9 +2463,9 @@ function transformSpecFileCommentResponse(raw) {
2368
2463
  body: raw.body,
2369
2464
  deleted: raw.deleted,
2370
2465
  contentVersion: raw.content_version,
2371
- createdAt: parseTimestamp(raw.created_at),
2372
- updatedAt: parseTimestamp(raw.updated_at),
2373
- deletedAt: parseTimestamp(raw.deleted_at)
2466
+ createdAt: parseTimestamp2(raw.created_at),
2467
+ updatedAt: parseTimestamp2(raw.updated_at),
2468
+ deletedAt: parseTimestamp2(raw.deleted_at)
2374
2469
  };
2375
2470
  }
2376
2471
  function transformSpecFileCommentThreadResponse(raw) {
@@ -2390,10 +2485,10 @@ function transformSpecFileCommentThreadResponse(raw) {
2390
2485
  anchorStatus: toAnchorStatus(raw.anchor_status),
2391
2486
  createdBy: raw.created_by,
2392
2487
  createdByKind: toCommentActorKind(raw.created_by_kind),
2393
- resolvedAt: parseTimestamp(raw.resolved_at),
2488
+ resolvedAt: parseTimestamp2(raw.resolved_at),
2394
2489
  resolvedBy: raw.resolved_by ?? null,
2395
- createdAt: parseTimestamp(raw.created_at),
2396
- updatedAt: parseTimestamp(raw.updated_at),
2490
+ createdAt: parseTimestamp2(raw.created_at),
2491
+ updatedAt: parseTimestamp2(raw.updated_at),
2397
2492
  // Always an array: consumers map over it on every render, so the empty case
2398
2493
  // must not be a value each one has to guard.
2399
2494
  comments: (raw.comments ?? []).map(transformSpecFileCommentResponse)
@@ -3152,6 +3247,40 @@ var HttpSpecFileCommentClient = class {
3152
3247
  }
3153
3248
  };
3154
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
+
3155
3284
  // ../../packages/platform-client/artifact/types/artifact.types.ts
3156
3285
  function transformArtifactResponse(raw) {
3157
3286
  return {
@@ -3283,14 +3412,14 @@ function buildUpdateArtifactBody(request) {
3283
3412
  // ../../packages/platform-client/artifact/http/artifact.http-client.ts
3284
3413
  var BASE_PATH = "/artifact/v1";
3285
3414
  var HEADER_EXPECTED_VERSION3 = "X-Expected-Version";
3286
- var HEADER_ACTOR_KIND3 = "X-Actor-Kind";
3415
+ var HEADER_ACTOR_KIND4 = "X-Actor-Kind";
3287
3416
  function writeHeaders(opts) {
3288
3417
  const headers = {};
3289
3418
  if (opts?.expectedVersion !== void 0) {
3290
3419
  headers[HEADER_EXPECTED_VERSION3] = String(opts.expectedVersion);
3291
3420
  }
3292
3421
  if (opts?.actorKind) {
3293
- headers[HEADER_ACTOR_KIND3] = opts.actorKind;
3422
+ headers[HEADER_ACTOR_KIND4] = opts.actorKind;
3294
3423
  }
3295
3424
  return headers;
3296
3425
  }
@@ -3868,7 +3997,8 @@ var PlatformClient = class {
3868
3997
  attachment: new HttpAttachmentClient(httpClient),
3869
3998
  artifact: new HttpArtifactClient(httpClient),
3870
3999
  streamToken: new HttpStreamTokenClient(httpClient),
3871
- comment: new HttpSpecFileCommentClient(httpClient)
4000
+ comment: new HttpSpecFileCommentClient(httpClient),
4001
+ task: new HttpProjectTaskClient(httpClient)
3872
4002
  };
3873
4003
  }
3874
4004
  // ------------------------------------------------------- stream tokens
@@ -3972,6 +4102,26 @@ var PlatformClient = class {
3972
4102
  (jwt, c) => c.comment.deleteComment(fileId, commentId, jwt, { actorKind: "ai" })
3973
4103
  );
3974
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
+ }
3975
4125
  async withTokenRetry(call) {
3976
4126
  const clients = await this.clients();
3977
4127
  const jwt = await this.tokenManager.getValidAccessToken();
@@ -7180,6 +7330,179 @@ var startSpecSessionTool = {
7180
7330
  }
7181
7331
  };
7182
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
+
7183
7506
  // src/server/server.ts
7184
7507
  function buildServer(deps) {
7185
7508
  const server = new McpServer({
@@ -7231,6 +7554,9 @@ function buildServer(deps) {
7231
7554
  registerTool(server, replySpecFileCommentThreadTool, ctx);
7232
7555
  registerTool(server, updateSpecFileCommentTool, ctx);
7233
7556
  registerTool(server, deleteSpecFileCommentTool, ctx);
7557
+ registerTool(server, listTasksTool, ctx);
7558
+ registerTool(server, updateTasksTool, ctx);
7559
+ registerTool(server, setTaskStatusTool, ctx);
7234
7560
  return server;
7235
7561
  }
7236
7562
  async function startStdioServer(deps) {
@@ -7402,7 +7728,7 @@ async function canonicaliseRoot(root) {
7402
7728
  import { promises as fs4 } from "fs";
7403
7729
  import { join as join2, relative } from "path";
7404
7730
  import { spawn } from "child_process";
7405
- import { z as z39 } from "zod";
7731
+ import { z as z40 } from "zod";
7406
7732
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
7407
7733
 
7408
7734
  // src/reverse/ignore.ts
@@ -7707,37 +8033,37 @@ var READ_FILE_MAX_LINES = 2e3;
7707
8033
  var LIST_DIR_HARD_CAP = 5e3;
7708
8034
  var LIST_DIR_DEFAULT_CAP = 1e3;
7709
8035
  var GREP_MAX_MATCHES_DEFAULT = 200;
7710
- var listDirInput = z39.object({
7711
- path: z39.string(),
7712
- recursive: z39.boolean().optional(),
7713
- maxEntries: z39.number().int().positive().optional(),
7714
- 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()
7715
8041
  });
7716
- var readFileInput = z39.object({
7717
- path: z39.string(),
7718
- offset: z39.number().int().nonnegative().optional(),
7719
- limit: z39.number().int().positive().optional(),
7720
- 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()
7721
8047
  });
7722
- var grepInput = z39.object({
7723
- pattern: z39.string().describe(
8048
+ var grepInput = z40.object({
8049
+ pattern: z40.string().describe(
7724
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."
7725
8051
  ),
7726
- path: z39.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
7727
- isRegex: z39.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
7728
- caseSensitive: z39.boolean().optional(),
7729
- maxMatches: z39.number().int().positive().optional(),
7730
- contextLines: z39.number().int().nonnegative().optional(),
7731
- 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()
7732
8058
  });
7733
- var packCodebaseInput = z39.object({
7734
- subpath: z39.string().optional(),
7735
- includePatterns: z39.string().optional(),
7736
- ignorePatterns: z39.string().optional()
8059
+ var packCodebaseInput = z40.object({
8060
+ subpath: z40.string().optional(),
8061
+ includePatterns: z40.string().optional(),
8062
+ ignorePatterns: z40.string().optional()
7737
8063
  });
7738
- var packCodebaseReadPageInput = z39.object({
7739
- outputId: z39.string(),
7740
- page: z39.number().int().positive()
8064
+ var packCodebaseReadPageInput = z40.object({
8065
+ outputId: z40.string(),
8066
+ page: z40.number().int().positive()
7741
8067
  });
7742
8068
  function structuredError(error, message, extra) {
7743
8069
  const payload = { error, message, ...extra ?? {} };
@@ -8265,7 +8591,7 @@ function jsonSchemaFromZod(schema) {
8265
8591
  return zodToJson(schema);
8266
8592
  }
8267
8593
  function zodToJson(schema) {
8268
- if (schema instanceof z39.ZodObject) {
8594
+ if (schema instanceof z40.ZodObject) {
8269
8595
  const shape = schema.shape;
8270
8596
  const properties = {};
8271
8597
  const required = [];
@@ -8280,19 +8606,19 @@ function zodToJson(schema) {
8280
8606
  if (required.length > 0) out.required = required;
8281
8607
  return out;
8282
8608
  }
8283
- if (schema instanceof z39.ZodOptional) {
8609
+ if (schema instanceof z40.ZodOptional) {
8284
8610
  return zodToJson(schema.unwrap());
8285
8611
  }
8286
- if (schema instanceof z39.ZodString) {
8612
+ if (schema instanceof z40.ZodString) {
8287
8613
  return { type: "string" };
8288
8614
  }
8289
- if (schema instanceof z39.ZodNumber) {
8615
+ if (schema instanceof z40.ZodNumber) {
8290
8616
  return { type: "number" };
8291
8617
  }
8292
- if (schema instanceof z39.ZodBoolean) {
8618
+ if (schema instanceof z40.ZodBoolean) {
8293
8619
  return { type: "boolean" };
8294
8620
  }
8295
- if (schema instanceof z39.ZodEnum) {
8621
+ if (schema instanceof z40.ZodEnum) {
8296
8622
  return { type: "string", enum: schema.options };
8297
8623
  }
8298
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.164",
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": {