@myspec/mcp-server 1.1.1-next.165 → 1.1.1-next.166
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 +4 -3
- package/dist/index.js +224 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -363,9 +363,10 @@ 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
|
|
367
|
-
| `
|
|
368
|
-
| `
|
|
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` |
|
|
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 |
|
|
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 |
|
|
369
370
|
|
|
370
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.
|
|
371
372
|
|
package/dist/index.js
CHANGED
|
@@ -1841,6 +1841,24 @@ function isProjectPermissionDeniedError(err) {
|
|
|
1841
1841
|
const message = err.message;
|
|
1842
1842
|
return typeof message === "string" && message.includes("PROJECT_PERMISSION_DENIED:");
|
|
1843
1843
|
}
|
|
1844
|
+
var TASK_BOARD_DISABLED_CODE = "task_board_disabled";
|
|
1845
|
+
var PlatformTaskBoardDisabledError = class extends PlatformHttpError {
|
|
1846
|
+
code = TASK_BOARD_DISABLED_CODE;
|
|
1847
|
+
constructor(message, details) {
|
|
1848
|
+
super(message, 403, details);
|
|
1849
|
+
this.name = "PlatformTaskBoardDisabledError";
|
|
1850
|
+
}
|
|
1851
|
+
};
|
|
1852
|
+
function isTaskBoardDisabledError(err) {
|
|
1853
|
+
if (err instanceof PlatformTaskBoardDisabledError) {
|
|
1854
|
+
return true;
|
|
1855
|
+
}
|
|
1856
|
+
if (typeof err !== "object" || err === null) {
|
|
1857
|
+
return false;
|
|
1858
|
+
}
|
|
1859
|
+
const code = err.code;
|
|
1860
|
+
return typeof code === "string" && code.toLowerCase() === TASK_BOARD_DISABLED_CODE;
|
|
1861
|
+
}
|
|
1844
1862
|
function isPlatformHttpError(err) {
|
|
1845
1863
|
return typeof err === "object" && err !== null && err[PLATFORM_ERROR_BRAND] === true;
|
|
1846
1864
|
}
|
|
@@ -1933,6 +1951,9 @@ function createPlatformError(message, status, details, code) {
|
|
|
1933
1951
|
if (status === 403 && typeof code === "string" && code.toLowerCase() === PROJECT_PERMISSION_DENIED_CODE) {
|
|
1934
1952
|
return new PlatformPermissionError(message, details);
|
|
1935
1953
|
}
|
|
1954
|
+
if (status === 403 && typeof code === "string" && code.toLowerCase() === TASK_BOARD_DISABLED_CODE) {
|
|
1955
|
+
return new PlatformTaskBoardDisabledError(message, details);
|
|
1956
|
+
}
|
|
1936
1957
|
return new PlatformHttpError(message, status, details);
|
|
1937
1958
|
}
|
|
1938
1959
|
function truncate(text) {
|
|
@@ -1958,6 +1979,23 @@ async function throwFromResponse(response, fallbackMessage) {
|
|
|
1958
1979
|
throw createPlatformError(`HTTP ${response.status}: ${message}`, response.status, details, code);
|
|
1959
1980
|
}
|
|
1960
1981
|
|
|
1982
|
+
// ../../packages/platform-client/shared/jwt.ts
|
|
1983
|
+
function jwtSubject(token) {
|
|
1984
|
+
const raw = token.startsWith("Bearer ") ? token.slice("Bearer ".length) : token;
|
|
1985
|
+
const payloadSegment = raw.split(".")[1];
|
|
1986
|
+
if (!payloadSegment) {
|
|
1987
|
+
return null;
|
|
1988
|
+
}
|
|
1989
|
+
try {
|
|
1990
|
+
const base64 = payloadSegment.replace(/-/g, "+").replace(/_/g, "/");
|
|
1991
|
+
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
1992
|
+
const payload = JSON.parse(atob(padded));
|
|
1993
|
+
return typeof payload.sub === "string" && payload.sub !== "" ? payload.sub : null;
|
|
1994
|
+
} catch {
|
|
1995
|
+
return null;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1961
1999
|
// ../../packages/shared/logger.ts
|
|
1962
2000
|
var LOG_LEVEL_PRIORITY = {
|
|
1963
2001
|
debug: 0,
|
|
@@ -2325,7 +2363,12 @@ var TASK_LIMITS = {
|
|
|
2325
2363
|
maxLabels: 20,
|
|
2326
2364
|
maxLabelLength: 40,
|
|
2327
2365
|
maxStoryPoints: 9999.99,
|
|
2328
|
-
|
|
2366
|
+
maxCycle: 1e5,
|
|
2367
|
+
maxTasksPerRequest: 200,
|
|
2368
|
+
/** The most tasks one page of a listing holds. */
|
|
2369
|
+
maxPageSize: 500,
|
|
2370
|
+
/** The most tasks.md files one board lists (and the most expected_versions a status change takes). */
|
|
2371
|
+
maxBoardFiles: 500
|
|
2329
2372
|
};
|
|
2330
2373
|
function parseTimestamp(raw) {
|
|
2331
2374
|
if (!raw) return null;
|
|
@@ -2380,9 +2423,31 @@ function transformTaskBoard(raw) {
|
|
|
2380
2423
|
})),
|
|
2381
2424
|
tasks: (raw.tasks ?? []).map(transformProjectTask),
|
|
2382
2425
|
labels: raw.labels ?? [],
|
|
2383
|
-
cycles: raw.cycles ?? []
|
|
2426
|
+
cycles: raw.cycles ?? [],
|
|
2427
|
+
filesTruncated: raw.files_truncated === true,
|
|
2428
|
+
...typeof raw.total === "number" ? {
|
|
2429
|
+
page: {
|
|
2430
|
+
total: raw.total,
|
|
2431
|
+
offset: raw.offset ?? 0,
|
|
2432
|
+
limit: raw.limit ?? 0,
|
|
2433
|
+
nextOffset: typeof raw.next_offset === "number" ? raw.next_offset : null
|
|
2434
|
+
}
|
|
2435
|
+
} : {}
|
|
2384
2436
|
};
|
|
2385
2437
|
}
|
|
2438
|
+
function toTaskListQueryString(query) {
|
|
2439
|
+
if (!query) return "";
|
|
2440
|
+
const params = new URLSearchParams();
|
|
2441
|
+
if (query.status) params.set("status", query.status);
|
|
2442
|
+
if (query.assigneeUserId) params.set("assignee_user_id", query.assigneeUserId);
|
|
2443
|
+
if (query.cycle !== void 0) params.set("cycle", String(query.cycle));
|
|
2444
|
+
if (query.label) params.set("label", query.label);
|
|
2445
|
+
if (query.specFileId) params.set("spec_file_id", query.specFileId);
|
|
2446
|
+
if (query.limit !== void 0) params.set("limit", String(query.limit));
|
|
2447
|
+
if (query.offset !== void 0) params.set("offset", String(query.offset));
|
|
2448
|
+
const qs = params.toString();
|
|
2449
|
+
return qs ? `?${qs}` : "";
|
|
2450
|
+
}
|
|
2386
2451
|
function transformSetTaskStatusResult(raw) {
|
|
2387
2452
|
return {
|
|
2388
2453
|
results: (raw.results ?? []).map((r) => ({
|
|
@@ -2399,6 +2464,17 @@ function transformSetTaskStatusResult(raw) {
|
|
|
2399
2464
|
tasks: (raw.tasks ?? []).map(transformProjectTask)
|
|
2400
2465
|
};
|
|
2401
2466
|
}
|
|
2467
|
+
function transformTaskAssigneeList(raw) {
|
|
2468
|
+
return {
|
|
2469
|
+
assignees: (raw.assignees ?? []).map((a) => ({
|
|
2470
|
+
userId: a.user_id,
|
|
2471
|
+
username: a.username ?? "",
|
|
2472
|
+
displayName: a.display_name ?? ""
|
|
2473
|
+
})),
|
|
2474
|
+
selfUserId: raw.self_user_id ?? "",
|
|
2475
|
+
truncated: raw.truncated === true
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2402
2478
|
function toRawUpdateTasksRequest(taskIds, patch) {
|
|
2403
2479
|
const set = {};
|
|
2404
2480
|
if ("assigneeUserId" in patch) set.assignee_user_id = patch.assigneeUserId ?? null;
|
|
@@ -3257,10 +3333,18 @@ var HttpProjectTaskClient = class {
|
|
|
3257
3333
|
constructor(httpClient) {
|
|
3258
3334
|
this.httpClient = httpClient;
|
|
3259
3335
|
}
|
|
3260
|
-
async listProjectTasks(projectId, jwtToken) {
|
|
3261
|
-
const raw = await this.httpClient.get(
|
|
3336
|
+
async listProjectTasks(projectId, jwtToken, query) {
|
|
3337
|
+
const raw = await this.httpClient.get(
|
|
3338
|
+
`${tasksPath(projectId)}${toTaskListQueryString(query)}`,
|
|
3339
|
+
jwtToken,
|
|
3340
|
+
{}
|
|
3341
|
+
);
|
|
3262
3342
|
return transformTaskBoard(raw);
|
|
3263
3343
|
}
|
|
3344
|
+
async listProjectTaskAssignees(projectId, jwtToken) {
|
|
3345
|
+
const raw = await this.httpClient.get(`${tasksPath(projectId)}/assignees`, jwtToken, {});
|
|
3346
|
+
return transformTaskAssigneeList(raw);
|
|
3347
|
+
}
|
|
3264
3348
|
async updateProjectTasks(projectId, taskIds, patch, jwtToken) {
|
|
3265
3349
|
const raw = await this.httpClient.patch(
|
|
3266
3350
|
tasksPath(projectId),
|
|
@@ -3271,9 +3355,16 @@ var HttpProjectTaskClient = class {
|
|
|
3271
3355
|
return (raw.tasks ?? []).map(transformProjectTask);
|
|
3272
3356
|
}
|
|
3273
3357
|
async setProjectTaskStatus(projectId, taskIds, status, jwtToken, opts) {
|
|
3358
|
+
const body = {
|
|
3359
|
+
task_ids: taskIds,
|
|
3360
|
+
status
|
|
3361
|
+
};
|
|
3362
|
+
if (opts?.expectedVersions && Object.keys(opts.expectedVersions).length > 0) {
|
|
3363
|
+
body.expected_versions = opts.expectedVersions;
|
|
3364
|
+
}
|
|
3274
3365
|
const raw = await this.httpClient.post(
|
|
3275
3366
|
`${tasksPath(projectId)}/status`,
|
|
3276
|
-
|
|
3367
|
+
body,
|
|
3277
3368
|
jwtToken,
|
|
3278
3369
|
opts?.actorKind ? { headers: { [HEADER_ACTOR_KIND3]: opts.actorKind } } : {}
|
|
3279
3370
|
);
|
|
@@ -4108,18 +4199,33 @@ var PlatformClient = class {
|
|
|
4108
4199
|
// `actorKind: 'ai'` like every other file write here: the revision is then
|
|
4109
4200
|
// recorded as the assistant's, not filed under the user's name. A planning
|
|
4110
4201
|
// edit writes no file and the platform records no actor kind for it.
|
|
4111
|
-
/**
|
|
4112
|
-
async listProjectTasks(projectId) {
|
|
4113
|
-
return this.withTokenRetry((jwt, c) => c.task.listProjectTasks(projectId, jwt));
|
|
4202
|
+
/** The live tasks of a project's tasks.md files: all, or those `query` keeps. */
|
|
4203
|
+
async listProjectTasks(projectId, query) {
|
|
4204
|
+
return this.withTokenRetry((jwt, c) => c.task.listProjectTasks(projectId, jwt, query));
|
|
4205
|
+
}
|
|
4206
|
+
/** Who the project's tasks can be assigned to, and the caller's own user id. */
|
|
4207
|
+
async listProjectTaskAssignees(projectId) {
|
|
4208
|
+
return this.withTokenRetry((jwt, c) => c.task.listProjectTaskAssignees(projectId, jwt));
|
|
4209
|
+
}
|
|
4210
|
+
/**
|
|
4211
|
+
* The caller's own user id, read from the access token (unverified: only to
|
|
4212
|
+
* turn "me" into an id the platform then checks). Null when the token does
|
|
4213
|
+
* not carry one.
|
|
4214
|
+
*/
|
|
4215
|
+
async currentUserId() {
|
|
4216
|
+
return jwtSubject(await this.tokenManager.getValidAccessToken());
|
|
4114
4217
|
}
|
|
4115
4218
|
/** Applies one planning patch to up to 200 tasks; all or nothing. */
|
|
4116
4219
|
async updateProjectTasks(projectId, taskIds, patch) {
|
|
4117
4220
|
return this.withTokenRetry((jwt, c) => c.task.updateProjectTasks(projectId, taskIds, patch, jwt));
|
|
4118
4221
|
}
|
|
4119
|
-
/**
|
|
4120
|
-
|
|
4222
|
+
/**
|
|
4223
|
+
* Sets the status of up to 200 tasks by rewriting their checkboxes.
|
|
4224
|
+
* `expectedVersions` pins the file versions the caller's view came from.
|
|
4225
|
+
*/
|
|
4226
|
+
async setProjectTaskStatus(projectId, taskIds, status, expectedVersions) {
|
|
4121
4227
|
return this.withTokenRetry(
|
|
4122
|
-
(jwt, c) => c.task.setProjectTaskStatus(projectId, taskIds, status, jwt, { actorKind: "ai" })
|
|
4228
|
+
(jwt, c) => c.task.setProjectTaskStatus(projectId, taskIds, status, jwt, { actorKind: "ai", expectedVersions })
|
|
4123
4229
|
);
|
|
4124
4230
|
}
|
|
4125
4231
|
async withTokenRetry(call) {
|
|
@@ -7334,8 +7440,10 @@ var startSpecSessionTool = {
|
|
|
7334
7440
|
import { z as z39 } from "zod";
|
|
7335
7441
|
var LIST_DEFAULT_TASKS = 100;
|
|
7336
7442
|
var LIST_MAX_TASKS = 500;
|
|
7443
|
+
var LIST_MAX_OFFSET = 1e6;
|
|
7337
7444
|
var DETAILS_PREVIEW_CHARS = 500;
|
|
7338
|
-
var
|
|
7445
|
+
var ME = "me";
|
|
7446
|
+
var NONE = "none";
|
|
7339
7447
|
var PRIORITY_NAMES = ["none", "urgent", "high", "medium", "low"];
|
|
7340
7448
|
var STATUS = z39.enum(["pending", "in_progress", "done"]);
|
|
7341
7449
|
function toDay(date) {
|
|
@@ -7371,6 +7479,10 @@ function taskSummary(task, opts = {}) {
|
|
|
7371
7479
|
};
|
|
7372
7480
|
}
|
|
7373
7481
|
var userIdField = z39.string().trim().min(1);
|
|
7482
|
+
async function resolveMe(id, ctx) {
|
|
7483
|
+
return id === ME ? ctx.client.currentUserId() : id;
|
|
7484
|
+
}
|
|
7485
|
+
var UNKNOWN_ME = 'Could not tell who you are from this connection, so "me" cannot be used. Pass your user id instead: list_task_assignees returns it as self_user_id.';
|
|
7374
7486
|
var labelField = z39.string().trim().min(1).refine((label) => Array.from(label).length <= TASK_LIMITS.maxLabelLength, {
|
|
7375
7487
|
message: `A label is at most ${String(TASK_LIMITS.maxLabelLength)} characters.`
|
|
7376
7488
|
});
|
|
@@ -7380,8 +7492,10 @@ function taskIdsField(what) {
|
|
|
7380
7492
|
var listInputSchema2 = {
|
|
7381
7493
|
project_id: z39.uuid().describe("UUID of the project whose tasks to list."),
|
|
7382
7494
|
status: STATUS.optional().describe("Only tasks with this status."),
|
|
7383
|
-
assignee_user_id: userIdField.optional().describe(
|
|
7384
|
-
|
|
7495
|
+
assignee_user_id: userIdField.optional().describe(
|
|
7496
|
+
'Only tasks assigned to this user id (as returned in assignee_user_id or by list_task_assignees). "me" means you; "none" means unassigned tasks.'
|
|
7497
|
+
),
|
|
7498
|
+
cycle: z39.number().int().min(1).max(TASK_LIMITS.maxCycle).optional().describe("Only tasks in this cycle (sprint) number."),
|
|
7385
7499
|
label: labelField.optional().describe("Only tasks carrying this label."),
|
|
7386
7500
|
file_id: z39.uuid().optional().describe("Only tasks from this tasks.md (its spec file id)."),
|
|
7387
7501
|
include_details: z39.boolean().optional().describe(
|
|
@@ -7390,20 +7504,51 @@ var listInputSchema2 = {
|
|
|
7390
7504
|
limit: z39.number().int().min(1).max(LIST_MAX_TASKS).optional().describe(
|
|
7391
7505
|
`Tasks per page (default ${String(LIST_DEFAULT_TASKS)}, at most ${String(LIST_MAX_TASKS)}).`
|
|
7392
7506
|
),
|
|
7393
|
-
offset: z39.number().int().min(0).optional().describe("How many matching tasks to skip; pass next_offset from the previous page.")
|
|
7507
|
+
offset: z39.number().int().min(0).max(LIST_MAX_OFFSET).optional().describe("How many matching tasks to skip; pass next_offset from the previous page.")
|
|
7394
7508
|
};
|
|
7509
|
+
function pageLocally(board, query) {
|
|
7510
|
+
const assignee = query.assigneeUserId;
|
|
7511
|
+
const label = query.label?.split(/\s+/).filter(Boolean).join(" ");
|
|
7512
|
+
const matching = board.tasks.filter(
|
|
7513
|
+
(task) => (query.status === void 0 || task.status === query.status) && (assignee === void 0 || (assignee === NONE ? task.assigneeUserId === null : task.assigneeUserId === assignee)) && (query.cycle === void 0 || task.cycle === query.cycle) && (label === void 0 || task.labels.includes(label)) && (query.specFileId === void 0 || task.specFileId === query.specFileId)
|
|
7514
|
+
);
|
|
7515
|
+
const offset = query.offset ?? 0;
|
|
7516
|
+
const tasks = matching.slice(offset, offset + (query.limit ?? LIST_DEFAULT_TASKS));
|
|
7517
|
+
const next = offset + tasks.length;
|
|
7518
|
+
return {
|
|
7519
|
+
tasks,
|
|
7520
|
+
total: matching.length,
|
|
7521
|
+
offset,
|
|
7522
|
+
nextOffset: next < matching.length ? next : null
|
|
7523
|
+
};
|
|
7524
|
+
}
|
|
7395
7525
|
var listTasksTool = {
|
|
7396
7526
|
name: "list_tasks",
|
|
7397
|
-
description:
|
|
7527
|
+
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.`,
|
|
7398
7528
|
inputSchema: listInputSchema2,
|
|
7399
7529
|
handler: async (args, ctx) => {
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
const
|
|
7530
|
+
let assignee;
|
|
7531
|
+
if (args.assignee_user_id !== void 0) {
|
|
7532
|
+
const resolved = await resolveMe(args.assignee_user_id, ctx);
|
|
7533
|
+
if (resolved === null) return errorResult(UNKNOWN_ME);
|
|
7534
|
+
assignee = resolved;
|
|
7535
|
+
}
|
|
7536
|
+
const query = {
|
|
7537
|
+
...args.status !== void 0 ? { status: args.status } : {},
|
|
7538
|
+
...assignee !== void 0 ? { assigneeUserId: assignee } : {},
|
|
7539
|
+
...args.cycle !== void 0 ? { cycle: args.cycle } : {},
|
|
7540
|
+
...args.label !== void 0 ? { label: args.label } : {},
|
|
7541
|
+
...args.file_id !== void 0 ? { specFileId: args.file_id } : {},
|
|
7542
|
+
limit: args.limit ?? LIST_DEFAULT_TASKS,
|
|
7543
|
+
offset: args.offset ?? 0
|
|
7544
|
+
};
|
|
7545
|
+
const board = await ctx.client.listProjectTasks(args.project_id, query);
|
|
7546
|
+
const page = board.page ? {
|
|
7547
|
+
tasks: board.tasks,
|
|
7548
|
+
total: board.page.total,
|
|
7549
|
+
offset: board.page.offset,
|
|
7550
|
+
nextOffset: board.page.nextOffset
|
|
7551
|
+
} : pageLocally(board, query);
|
|
7407
7552
|
return jsonResult({
|
|
7408
7553
|
project_id: board.projectId,
|
|
7409
7554
|
files: board.files.map((file) => ({
|
|
@@ -7413,25 +7558,49 @@ var listTasksTool = {
|
|
|
7413
7558
|
// The file could not be re-read just now: its tasks are from the last read.
|
|
7414
7559
|
...file.stale ? { stale: true } : {}
|
|
7415
7560
|
})),
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7561
|
+
// Only ever present and true: the project has more than 500 tasks.md
|
|
7562
|
+
// files and the rest are left out.
|
|
7563
|
+
...board.filesTruncated ? { files_truncated: true } : {},
|
|
7564
|
+
tasks: page.tasks.map((task) => taskSummary(task, { details: args.include_details })),
|
|
7565
|
+
total_tasks: page.total,
|
|
7566
|
+
offset: page.offset,
|
|
7419
7567
|
// Only while more remain, so its absence means "that was the last page".
|
|
7420
|
-
...
|
|
7568
|
+
...page.nextOffset !== null ? { next_offset: page.nextOffset } : {},
|
|
7421
7569
|
labels: board.labels,
|
|
7422
7570
|
cycles: board.cycles
|
|
7423
7571
|
});
|
|
7424
7572
|
}
|
|
7425
7573
|
};
|
|
7574
|
+
var assigneesInputSchema = {
|
|
7575
|
+
project_id: z39.uuid().describe("UUID of the project whose tasks are being assigned.")
|
|
7576
|
+
};
|
|
7577
|
+
var listTaskAssigneesTool = {
|
|
7578
|
+
name: "list_task_assignees",
|
|
7579
|
+
description: `List who a project's tasks can be assigned to: the organisation's people who can open the project, which is exactly who update_tasks accepts as assignee_user_id. Each has a user_id, a username (handle) and a display_name ("" when unset). Also returns your own user id as self_user_id. May include people who have left the organisation but whose profile remains. At most 1000 people; truncated is true when the list was cut.`,
|
|
7580
|
+
inputSchema: assigneesInputSchema,
|
|
7581
|
+
handler: async (args, ctx) => {
|
|
7582
|
+
const list = await ctx.client.listProjectTaskAssignees(args.project_id);
|
|
7583
|
+
return jsonResult({
|
|
7584
|
+
assignees: list.assignees.map((a) => ({
|
|
7585
|
+
user_id: a.userId,
|
|
7586
|
+
username: a.username,
|
|
7587
|
+
display_name: a.displayName
|
|
7588
|
+
})),
|
|
7589
|
+
self_user_id: list.selfUserId,
|
|
7590
|
+
// Only ever present and true.
|
|
7591
|
+
...list.truncated ? { truncated: true } : {}
|
|
7592
|
+
});
|
|
7593
|
+
}
|
|
7594
|
+
};
|
|
7426
7595
|
var updateInputSchema2 = {
|
|
7427
7596
|
project_id: z39.uuid().describe("UUID of the project the tasks belong to."),
|
|
7428
7597
|
task_ids: taskIdsField("The tasks to change (task_id values from list_tasks)."),
|
|
7429
7598
|
assignee_user_id: userIdField.nullable().optional().describe(
|
|
7430
|
-
|
|
7599
|
+
'User id to assign ("me" for yourself), or null to unassign. The person must be a member of the organisation who can open the project: list_task_assignees lists them.'
|
|
7431
7600
|
),
|
|
7432
7601
|
priority: z39.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low."),
|
|
7433
7602
|
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(
|
|
7603
|
+
cycle: z39.number().int().min(1).max(TASK_LIMITS.maxCycle).nullable().optional().describe("Cycle (sprint) number, or null to clear it."),
|
|
7435
7604
|
start_date: z39.iso.date().nullable().optional().describe("Start day as YYYY-MM-DD, or null to clear it."),
|
|
7436
7605
|
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
7606
|
add_labels: z39.array(labelField).max(TASK_LIMITS.maxLabels).optional().describe(`Labels to add. A task carries at most ${String(TASK_LIMITS.maxLabels)}.`),
|
|
@@ -7443,7 +7612,13 @@ var updateTasksTool = {
|
|
|
7443
7612
|
inputSchema: updateInputSchema2,
|
|
7444
7613
|
handler: async (args, ctx) => {
|
|
7445
7614
|
const patch = {};
|
|
7446
|
-
if (args.assignee_user_id
|
|
7615
|
+
if (args.assignee_user_id === null) {
|
|
7616
|
+
patch.assigneeUserId = null;
|
|
7617
|
+
} else if (args.assignee_user_id !== void 0) {
|
|
7618
|
+
const assignee = await resolveMe(args.assignee_user_id, ctx);
|
|
7619
|
+
if (assignee === null) return errorResult(UNKNOWN_ME);
|
|
7620
|
+
patch.assigneeUserId = assignee;
|
|
7621
|
+
}
|
|
7447
7622
|
if (args.priority !== void 0) patch.priority = args.priority;
|
|
7448
7623
|
if (args.story_points !== void 0) patch.storyPoints = args.story_points;
|
|
7449
7624
|
if (args.cycle !== void 0) patch.cycle = args.cycle;
|
|
@@ -7467,17 +7642,27 @@ var updateTasksTool = {
|
|
|
7467
7642
|
var statusInputSchema = {
|
|
7468
7643
|
project_id: z39.uuid().describe("UUID of the project the tasks belong to."),
|
|
7469
7644
|
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.")
|
|
7645
|
+
status: STATUS.describe("The new status: pending, in_progress or done."),
|
|
7646
|
+
expected_versions: z39.array(
|
|
7647
|
+
z39.object({
|
|
7648
|
+
spec_file_id: z39.uuid().describe("A tasks.md file (spec_file_id from list_tasks)."),
|
|
7649
|
+
content_version: z39.number().int().min(0).describe("The content_version list_tasks returned for that file.")
|
|
7650
|
+
})
|
|
7651
|
+
).max(TASK_LIMITS.maxBoardFiles).optional().describe(
|
|
7652
|
+
"Optional. The file versions your view of these tasks came from (from list_tasks); only the files these tasks are in are checked, so passing just those is enough. A file that has changed since is not written and its tasks fail with file_changed, so a change never lands on a line you did not see; list again and retry."
|
|
7653
|
+
)
|
|
7471
7654
|
};
|
|
7472
7655
|
var setTaskStatusTool = {
|
|
7473
7656
|
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.",
|
|
7657
|
+
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, it changed since the expected_versions you passed, 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
7658
|
inputSchema: statusInputSchema,
|
|
7476
7659
|
handler: async (args, ctx) => {
|
|
7660
|
+
const expected = args.expected_versions?.length ? Object.fromEntries(args.expected_versions.map((v) => [v.spec_file_id, v.content_version])) : void 0;
|
|
7477
7661
|
const result = await ctx.client.setProjectTaskStatus(
|
|
7478
7662
|
args.project_id,
|
|
7479
7663
|
args.task_ids,
|
|
7480
|
-
args.status
|
|
7664
|
+
args.status,
|
|
7665
|
+
expected
|
|
7481
7666
|
);
|
|
7482
7667
|
const count = (outcome) => result.results.filter((r) => r.outcome === outcome).length;
|
|
7483
7668
|
const failed = count("failed");
|
|
@@ -7555,6 +7740,7 @@ function buildServer(deps) {
|
|
|
7555
7740
|
registerTool(server, updateSpecFileCommentTool, ctx);
|
|
7556
7741
|
registerTool(server, deleteSpecFileCommentTool, ctx);
|
|
7557
7742
|
registerTool(server, listTasksTool, ctx);
|
|
7743
|
+
registerTool(server, listTaskAssigneesTool, ctx);
|
|
7558
7744
|
registerTool(server, updateTasksTool, ctx);
|
|
7559
7745
|
registerTool(server, setTaskStatusTool, ctx);
|
|
7560
7746
|
return server;
|
|
@@ -7575,6 +7761,11 @@ function wrapToolHandler(tool, ctx) {
|
|
|
7575
7761
|
if (isProjectPermissionDeniedError(err)) {
|
|
7576
7762
|
return errorResult(`${tool.name} failed: ${permissionDeniedMessage(err)}`);
|
|
7577
7763
|
}
|
|
7764
|
+
if (isTaskBoardDisabledError(err)) {
|
|
7765
|
+
return errorResult(
|
|
7766
|
+
`${tool.name} failed: the task board is turned off in this MySpec environment, so the task tools are unavailable here until it is turned on.`
|
|
7767
|
+
);
|
|
7768
|
+
}
|
|
7578
7769
|
const message = err instanceof Error ? err.message : String(err);
|
|
7579
7770
|
return errorResult(`${tool.name} failed: ${message}`);
|
|
7580
7771
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myspec/mcp-server",
|
|
3
|
-
"version": "1.1.1-next.
|
|
3
|
+
"version": "1.1.1-next.166",
|
|
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": {
|