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