@myspec/mcp-server 1.1.1-next.175 → 1.1.1-next.176

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.
package/README.md CHANGED
@@ -363,9 +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` (a user id, `me` or `none`), `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 (with the `content_version` their tasks reflect) and the labels and cycles in use. Filtered and paged by the platform: 100 tasks by default (`limit` up to 500), with `total_tasks` and, while more remain, `next_offset` |
366
+ | `list_tasks` | `project_id`, optional `status`, `assignee_user_id` (a user id, `me` or `none`), `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`, `relationships` (`{type, task_id}`: this task `blocks`, is `blocked_by`, or is `related` to that task), and with `include_details` a 500-character preview of its details. Also the files (with the `content_version` their tasks reflect) and the labels and cycles in use. Filtered and paged by the platform: 100 tasks by default (`limit` up to 500), with `total_tasks` and, while more remain, `next_offset` |
367
367
  | `list_task_assignees` | `project_id` | Who the project's tasks can be assigned to: the org's people who can open the project (exactly who `update_tasks` accepts), each with `user_id`, `username` and `display_name`, plus your own `self_user_id`. May include people who left the org but whose profile remains; at most 1000 (`truncated`) |
368
- | `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 (`list_task_assignees`); `me` means you. All or nothing: an unknown id fails the call. Returns the tasks as they are now |
368
+ | `update_tasks` | `project_id`, `task_ids[]` (≤ 200), any of `assignee_user_id`, `priority`, `story_points`, `cycle`, `start_date`, `end_date`, `add_labels[]`, `remove_labels[]`, `add_relationships[]` (`{type, task_id}`, ≤ 50), `remove_relationships[]` (task ids, ≤ 50) | Edits planning fields on every task given. A field left out is left alone; `null` clears it. Two tasks have at most one relationship: setting one replaces the pair's old one, and the other task sees the inverse. The assignee must be an org member who can open the project (`list_task_assignees`); `me` means you. All or nothing: an unknown id fails the call. Returns the tasks as they are now |
369
369
  | `set_task_status` | `project_id`, `task_ids[]` (≤ 200), `status`, optional `expected_versions[]` (`{spec_file_id, content_version}` from `list_tasks`) | Rewrites the tasks' checkboxes in tasks.md (`[ ]`, `[-]`, `[x]`), one revision per file, recorded as an AI change. A file that changed since the pinned version is not written (`file_changed`). 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 |
370
370
 
371
371
  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.
package/dist/index.js CHANGED
@@ -2390,12 +2390,15 @@ var SPEC_FILE_LOCK_TTL_MS = 2 * 6e4;
2390
2390
  // ../../packages/platform-client/project/types/task.types.ts
2391
2391
  var TASK_STATUSES = ["pending", "in_progress", "done"];
2392
2392
  var TASK_PRIORITIES = [0, 1, 2, 3, 4];
2393
+ var TASK_RELATIONSHIP_TYPES = ["blocks", "blocked_by", "related"];
2393
2394
  var TASK_LIMITS = {
2394
2395
  maxLabels: 20,
2395
2396
  maxLabelLength: 40,
2396
2397
  maxStoryPoints: 9999.99,
2397
2398
  maxCycle: 1e5,
2398
2399
  maxTasksPerRequest: 200,
2400
+ /** The most relationships one task has, and the most one change adds (or removes). */
2401
+ maxRelationships: 50,
2399
2402
  /** The most tasks one page of a listing holds. */
2400
2403
  maxPageSize: 500,
2401
2404
  /** The most tasks.md files one board lists (and the most expected_versions a status change takes). */
@@ -2414,6 +2417,11 @@ function toStatus(raw) {
2414
2417
  function toPriority(raw) {
2415
2418
  return TASK_PRIORITIES.includes(raw) ? raw : 0;
2416
2419
  }
2420
+ function toRelationships(raw) {
2421
+ return (raw ?? []).flatMap(
2422
+ (r) => TASK_RELATIONSHIP_TYPES.includes(r.type) ? [{ type: r.type, taskId: r.task_id }] : []
2423
+ );
2424
+ }
2417
2425
  function toOutcome(raw) {
2418
2426
  return raw === "updated" || raw === "unchanged" || raw === "removed" ? raw : "failed";
2419
2427
  }
@@ -2440,6 +2448,7 @@ function transformProjectTask(raw) {
2440
2448
  startAt: parseTimestamp(raw.start_at),
2441
2449
  endAt: parseTimestamp(raw.end_at),
2442
2450
  labels: raw.labels ?? [],
2451
+ relationships: toRelationships(raw.relationships),
2443
2452
  updatedBy: raw.updated_by ?? null,
2444
2453
  createdAt: parseTimestamp(raw.created_at),
2445
2454
  updatedAt: parseTimestamp(raw.updated_at)
@@ -2527,8 +2536,15 @@ function toRawUpdateTasksRequest(taskIds, patch) {
2527
2536
  const body = { task_ids: taskIds, set: toRawPlanningSet(patch) };
2528
2537
  if (patch.addLabels?.length) body.add_labels = patch.addLabels;
2529
2538
  if (patch.removeLabels?.length) body.remove_labels = patch.removeLabels;
2539
+ addRawRelationshipEdits(body, patch);
2530
2540
  return body;
2531
2541
  }
2542
+ function addRawRelationshipEdits(body, patch) {
2543
+ if (patch.addRelationships?.length) {
2544
+ body.add_relationships = patch.addRelationships.map((r) => ({ type: r.type, task_id: r.taskId }));
2545
+ }
2546
+ if (patch.removeRelationships?.length) body.remove_relationships = patch.removeRelationships;
2547
+ }
2532
2548
  function toRawPlanningSet(patch) {
2533
2549
  const set = {};
2534
2550
  if ("assigneeUserId" in patch) set.assignee_user_id = patch.assigneeUserId ?? null;
@@ -2548,6 +2564,7 @@ function toRawTaskChange(change) {
2548
2564
  if (Object.keys(set).length > 0) raw.set = set;
2549
2565
  if (patch.addLabels?.length) raw.add_labels = patch.addLabels;
2550
2566
  if (patch.removeLabels?.length) raw.remove_labels = patch.removeLabels;
2567
+ addRawRelationshipEdits(raw, patch);
2551
2568
  }
2552
2569
  return raw;
2553
2570
  }
@@ -7532,6 +7549,7 @@ var ME = "me";
7532
7549
  var NONE = "none";
7533
7550
  var PRIORITY_NAMES = ["none", "urgent", "high", "medium", "low"];
7534
7551
  var STATUS = z39.enum(["pending", "in_progress", "done"]);
7552
+ var RELATIONSHIP_TYPE = z39.enum(["blocks", "blocked_by", "related"]);
7535
7553
  function toDay(date) {
7536
7554
  return date ? date.toISOString().slice(0, 10) : null;
7537
7555
  }
@@ -7558,6 +7576,8 @@ function taskSummary(task, opts = {}) {
7558
7576
  start_date: toDay(task.startAt),
7559
7577
  end_date: toDay(task.endAt),
7560
7578
  labels: task.labels,
7579
+ // "this task <type> task_id": blocks, blocked_by or related.
7580
+ relationships: task.relationships.map((r) => ({ type: r.type, task_id: r.taskId })),
7561
7581
  ...withDetails ? { details: truncated ? task.body.slice(0, DETAILS_PREVIEW_CHARS) : task.body } : {},
7562
7582
  // Only ever present and true.
7563
7583
  ...truncated ? { details_truncated: true } : {},
@@ -7610,7 +7630,7 @@ function pageLocally(board, query) {
7610
7630
  }
7611
7631
  var listTasksTool = {
7612
7632
  name: "list_tasks",
7613
- 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 (with the content_version their tasks reflect), and the labels and cycles in use. Filter by status, assignee ("me" for yourself, "none" for unassigned), 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.`,
7633
+ 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), labels and relationships with other tasks ({type, task_id}: this task blocks, is blocked_by, or is related to that task). Also returns the files (with the content_version their tasks reflect), and the labels and cycles in use. Filter by status, assignee ("me" for yourself, "none" for unassigned), 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.`,
7614
7634
  inputSchema: listInputSchema2,
7615
7635
  handler: async (args, ctx) => {
7616
7636
  let assignee;
@@ -7690,11 +7710,22 @@ var updateInputSchema2 = {
7690
7710
  start_date: z39.iso.date().nullable().optional().describe("Start day as YYYY-MM-DD, or null to clear it."),
7691
7711
  end_date: z39.iso.date().nullable().optional().describe("Due day as YYYY-MM-DD, or null to clear it. Not before the start day."),
7692
7712
  add_labels: z39.array(labelField).max(TASK_LIMITS.maxLabels).optional().describe(`Labels to add. A task carries at most ${String(TASK_LIMITS.maxLabels)}.`),
7693
- remove_labels: z39.array(labelField).max(TASK_LIMITS.maxLabels).optional().describe("Labels to remove.")
7713
+ remove_labels: z39.array(labelField).max(TASK_LIMITS.maxLabels).optional().describe("Labels to remove."),
7714
+ add_relationships: z39.array(
7715
+ z39.object({
7716
+ type: RELATIONSHIP_TYPE.describe(
7717
+ "How each changed task stands to task_id: blocks (task_id waits for it), blocked_by (it waits for task_id) or related."
7718
+ ),
7719
+ task_id: z39.uuid().describe("The other task (a task_id from list_tasks).")
7720
+ })
7721
+ ).max(TASK_LIMITS.maxRelationships).optional().describe(
7722
+ `Relationships to set. Two tasks have at most one: setting one replaces whatever the pair had, and the other task sees the inverse (A blocks B is B blocked_by A). A task has at most ${String(TASK_LIMITS.maxRelationships)}.`
7723
+ ),
7724
+ remove_relationships: z39.array(z39.uuid()).max(TASK_LIMITS.maxRelationships).optional().describe("Task ids to unlink each changed task from, whatever the relationship.")
7694
7725
  };
7695
7726
  var updateTasksTool = {
7696
7727
  name: "update_tasks",
7697
- 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.",
7728
+ description: "Edit the planning fields of one or more tasks: assignee, priority, story points, cycle, start and due dates, labels, and relationships with other tasks (blocks, blocked_by, related). The same change applies to every task given. A field left out is left alone; null clears it. Labels and relationships 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.",
7698
7729
  inputSchema: updateInputSchema2,
7699
7730
  handler: async (args, ctx) => {
7700
7731
  const patch = {};
@@ -7716,9 +7747,16 @@ var updateTasksTool = {
7716
7747
  }
7717
7748
  if (args.add_labels?.length) patch.addLabels = args.add_labels;
7718
7749
  if (args.remove_labels?.length) patch.removeLabels = args.remove_labels;
7750
+ if (args.add_relationships?.length) {
7751
+ patch.addRelationships = args.add_relationships.map((r) => ({
7752
+ type: r.type,
7753
+ taskId: r.task_id
7754
+ }));
7755
+ }
7756
+ if (args.remove_relationships?.length) patch.removeRelationships = args.remove_relationships;
7719
7757
  if (Object.keys(patch).length === 0) {
7720
7758
  return errorResult(
7721
- "Nothing to change: pass at least one of assignee_user_id, priority, story_points, cycle, start_date, end_date, add_labels or remove_labels."
7759
+ "Nothing to change: pass at least one of assignee_user_id, priority, story_points, cycle, start_date, end_date, add_labels, remove_labels, add_relationships or remove_relationships."
7722
7760
  );
7723
7761
  }
7724
7762
  const tasks = await ctx.client.updateProjectTasks(args.project_id, args.task_ids, patch);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myspec/mcp-server",
3
- "version": "1.1.1-next.175",
3
+ "version": "1.1.1-next.176",
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": {