@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.
- package/README.md +3 -0
- package/dist/index.js +368 -42
- 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
|
|
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:
|
|
2372
|
-
updatedAt:
|
|
2373
|
-
deletedAt:
|
|
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:
|
|
2488
|
+
resolvedAt: parseTimestamp2(raw.resolved_at),
|
|
2394
2489
|
resolvedBy: raw.resolved_by ?? null,
|
|
2395
|
-
createdAt:
|
|
2396
|
-
updatedAt:
|
|
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
|
|
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[
|
|
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
|
|
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 =
|
|
7711
|
-
path:
|
|
7712
|
-
recursive:
|
|
7713
|
-
maxEntries:
|
|
7714
|
-
includeIgnored:
|
|
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 =
|
|
7717
|
-
path:
|
|
7718
|
-
offset:
|
|
7719
|
-
limit:
|
|
7720
|
-
includeIgnored:
|
|
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 =
|
|
7723
|
-
pattern:
|
|
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:
|
|
7727
|
-
isRegex:
|
|
7728
|
-
caseSensitive:
|
|
7729
|
-
maxMatches:
|
|
7730
|
-
contextLines:
|
|
7731
|
-
includeIgnored:
|
|
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 =
|
|
7734
|
-
subpath:
|
|
7735
|
-
includePatterns:
|
|
7736
|
-
ignorePatterns:
|
|
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 =
|
|
7739
|
-
outputId:
|
|
7740
|
-
page:
|
|
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
|
|
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
|
|
8609
|
+
if (schema instanceof z40.ZodOptional) {
|
|
8284
8610
|
return zodToJson(schema.unwrap());
|
|
8285
8611
|
}
|
|
8286
|
-
if (schema instanceof
|
|
8612
|
+
if (schema instanceof z40.ZodString) {
|
|
8287
8613
|
return { type: "string" };
|
|
8288
8614
|
}
|
|
8289
|
-
if (schema instanceof
|
|
8615
|
+
if (schema instanceof z40.ZodNumber) {
|
|
8290
8616
|
return { type: "number" };
|
|
8291
8617
|
}
|
|
8292
|
-
if (schema instanceof
|
|
8618
|
+
if (schema instanceof z40.ZodBoolean) {
|
|
8293
8619
|
return { type: "boolean" };
|
|
8294
8620
|
}
|
|
8295
|
-
if (schema instanceof
|
|
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.
|
|
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": {
|