@myspec/mcp-server 1.1.1-next.159 → 1.1.1-next.161
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 -2
- package/dist/index.js +81 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -333,8 +333,8 @@ Local state is stored under `~/.myspec/` (alongside the on-disk cache root), spl
|
|
|
333
333
|
|
|
334
334
|
| Name | Inputs | Output |
|
|
335
335
|
|---|---|---|
|
|
336
|
-
| `list_projects` | `query?`, `limit?`, `offset?` | Paginated list of projects |
|
|
337
|
-
| `get_project` | `project_id` | Project + active sessions + files + attachments |
|
|
336
|
+
| `list_projects` | `query?`, `limit?`, `offset?` | Paginated list of projects, each with your `viewer_role` (`read`/`write`/`admin`) when the platform reports it |
|
|
337
|
+
| `get_project` | `project_id` | Project (with your `viewer_role` when reported) + active sessions + files + attachments |
|
|
338
338
|
| `get_spec_file` | `file_id`, optional `include_download_url` | File metadata, including `content_version` (see [Concurrent edits](#concurrent-edits-content_version)); with `include_download_url=N` also returns a download URL targeting revision `N` |
|
|
339
339
|
| `read_spec_file` | `file_id`, optional `revision`, optional `offset`, optional `limit` | UTF-8 content paginated by line (`offset` is 1-based, default/cap 2000 lines, 1 MiB per response) with `content`, `start_line`, `end_line`, `read_lines`, `total_lines`, `truncated`, `next_offset`; plus `content_version` when reading the latest revision (errors for binary or files > 8 MiB — use `download_spec_file` for those). Cached on disk under `MYSPEC_DOWNLOAD_ROOT`, so paging through a file downloads it once |
|
|
340
340
|
| `download_spec_file` | `file_id`, optional `destination_path`, optional `revision`, optional `overwrite` | Downloads the file's raw bytes to a local file — by default under `.specs/` in the working directory (see [Download destinations](#download-destinations)). Handles binary files and files up to 50 MiB. Refuses to replace an existing file unless `overwrite=true`. Returns only metadata (`saved_path`, `bytes_written`, `revision_number`, `destination_source`) — **not** the content, so a large file can be saved locally and read in chunks |
|
|
@@ -366,6 +366,8 @@ Local state is stored under `~/.myspec/` (alongside the on-disk cache root), spl
|
|
|
366
366
|
|
|
367
367
|
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
368
|
|
|
369
|
+
**Project roles.** A tool that needs more than your role on the project (`read` is read-only; `write` also edits files, comments, attachments and artifacts and starts AI chat sessions; `admin` also renames, archives and deletes the project) fails with `permission denied — this action needs the <role> role on the project; you have <role>. Ask a project admin for access.` A new token will not help; a project admin has to raise your role.
|
|
370
|
+
|
|
369
371
|
### Brownfield sessions: start the bridge first
|
|
370
372
|
|
|
371
373
|
`start_spec_session` hands MySpec a prompt; it does **not** hand it your files.
|
package/dist/index.js
CHANGED
|
@@ -1810,6 +1810,37 @@ function isPlatformQuotaError(err) {
|
|
|
1810
1810
|
const code = err.code;
|
|
1811
1811
|
return typeof code === "string" && code.toLowerCase() === QUOTA_EXCEEDED_CODE;
|
|
1812
1812
|
}
|
|
1813
|
+
var PROJECT_PERMISSION_DENIED_CODE = "project_permission_denied";
|
|
1814
|
+
var PlatformPermissionError = class extends PlatformHttpError {
|
|
1815
|
+
code = PROJECT_PERMISSION_DENIED_CODE;
|
|
1816
|
+
/** The capability that was refused, e.g. `file.write`. */
|
|
1817
|
+
capability;
|
|
1818
|
+
/** The role it needs. */
|
|
1819
|
+
requiredRole;
|
|
1820
|
+
/** The role the caller has. */
|
|
1821
|
+
viewerRole;
|
|
1822
|
+
constructor(message, details) {
|
|
1823
|
+
super(message, 403, details);
|
|
1824
|
+
this.name = "PlatformPermissionError";
|
|
1825
|
+
this.capability = stringField(details, "capability");
|
|
1826
|
+
this.requiredRole = stringField(details, "required_role");
|
|
1827
|
+
this.viewerRole = stringField(details, "viewer_role");
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
function isProjectPermissionDeniedError(err) {
|
|
1831
|
+
if (err instanceof PlatformPermissionError) {
|
|
1832
|
+
return true;
|
|
1833
|
+
}
|
|
1834
|
+
if (typeof err !== "object" || err === null) {
|
|
1835
|
+
return false;
|
|
1836
|
+
}
|
|
1837
|
+
const code = err.code;
|
|
1838
|
+
if (typeof code === "string" && code.toLowerCase() === PROJECT_PERMISSION_DENIED_CODE) {
|
|
1839
|
+
return true;
|
|
1840
|
+
}
|
|
1841
|
+
const message = err.message;
|
|
1842
|
+
return typeof message === "string" && message.includes("PROJECT_PERMISSION_DENIED:");
|
|
1843
|
+
}
|
|
1813
1844
|
function isPlatformHttpError(err) {
|
|
1814
1845
|
return typeof err === "object" && err !== null && err[PLATFORM_ERROR_BRAND] === true;
|
|
1815
1846
|
}
|
|
@@ -1899,6 +1930,9 @@ function createPlatformError(message, status, details, code) {
|
|
|
1899
1930
|
if (status === 429 && typeof code === "string" && code.toLowerCase() === QUOTA_EXCEEDED_CODE) {
|
|
1900
1931
|
return new PlatformQuotaError(message, status, details, code.toLowerCase());
|
|
1901
1932
|
}
|
|
1933
|
+
if (status === 403 && typeof code === "string" && code.toLowerCase() === PROJECT_PERMISSION_DENIED_CODE) {
|
|
1934
|
+
return new PlatformPermissionError(message, details);
|
|
1935
|
+
}
|
|
1902
1936
|
return new PlatformHttpError(message, status, details);
|
|
1903
1937
|
}
|
|
1904
1938
|
function truncate(text) {
|
|
@@ -2149,6 +2183,22 @@ var BaseHttpClient = class {
|
|
|
2149
2183
|
}
|
|
2150
2184
|
};
|
|
2151
2185
|
|
|
2186
|
+
// ../../packages/platform-client/shared/project-access.types.ts
|
|
2187
|
+
var RANK = { none: 0, read: 1, write: 2, admin: 3 };
|
|
2188
|
+
function parseProjectRole(value) {
|
|
2189
|
+
if (typeof value !== "string") {
|
|
2190
|
+
return void 0;
|
|
2191
|
+
}
|
|
2192
|
+
const role = value.trim().toLowerCase();
|
|
2193
|
+
return Object.prototype.hasOwnProperty.call(RANK, role) ? role : void 0;
|
|
2194
|
+
}
|
|
2195
|
+
function parseViewerRole(value) {
|
|
2196
|
+
if (value === void 0 || value === null || value === "") {
|
|
2197
|
+
return void 0;
|
|
2198
|
+
}
|
|
2199
|
+
return parseProjectRole(value) ?? "read";
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2152
2202
|
// ../../packages/platform-client/project/types/attachment.types.ts
|
|
2153
2203
|
var MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
2154
2204
|
function transformAttachmentResponse(raw) {
|
|
@@ -2232,7 +2282,9 @@ function transformProjectResponse(raw) {
|
|
|
2232
2282
|
createdAt: new Date(raw.created_at),
|
|
2233
2283
|
updatedAt: new Date(raw.updated_at),
|
|
2234
2284
|
deletedAt: raw.deleted_at ? new Date(raw.deleted_at) : void 0,
|
|
2235
|
-
lastActivityAt: raw.last_activity_at ? new Date(raw.last_activity_at) : void 0
|
|
2285
|
+
lastActivityAt: raw.last_activity_at ? new Date(raw.last_activity_at) : void 0,
|
|
2286
|
+
viewerRole: parseViewerRole(raw.viewer_role),
|
|
2287
|
+
viewerCanManageAccess: typeof raw.viewer_can_manage_access === "boolean" ? raw.viewer_can_manage_access : void 0
|
|
2236
2288
|
};
|
|
2237
2289
|
}
|
|
2238
2290
|
|
|
@@ -2254,7 +2306,8 @@ function transformSpecSessionResponse(raw) {
|
|
|
2254
2306
|
archivedAt: raw.archived_at ? new Date(raw.archived_at) : void 0,
|
|
2255
2307
|
createdAt: new Date(raw.created_at),
|
|
2256
2308
|
updatedAt: new Date(raw.updated_at),
|
|
2257
|
-
deletedAt: raw.deleted_at ? new Date(raw.deleted_at) : void 0
|
|
2309
|
+
deletedAt: raw.deleted_at ? new Date(raw.deleted_at) : void 0,
|
|
2310
|
+
viewerRole: parseViewerRole(raw.viewer_role)
|
|
2258
2311
|
};
|
|
2259
2312
|
}
|
|
2260
2313
|
|
|
@@ -4094,6 +4147,9 @@ var deleteProjectTool = {
|
|
|
4094
4147
|
try {
|
|
4095
4148
|
await ctx.client.deleteProject(args.project_id);
|
|
4096
4149
|
} catch (err) {
|
|
4150
|
+
if (isProjectPermissionDeniedError(err)) {
|
|
4151
|
+
throw err;
|
|
4152
|
+
}
|
|
4097
4153
|
const message = err instanceof Error ? err.message : String(err);
|
|
4098
4154
|
return errorResult(`Failed to delete project ${args.project_id}: ${message}`);
|
|
4099
4155
|
}
|
|
@@ -4434,7 +4490,8 @@ var getProjectTool = {
|
|
|
4434
4490
|
description: project.description,
|
|
4435
4491
|
status: project.status,
|
|
4436
4492
|
created_at: project.createdAt,
|
|
4437
|
-
updated_at: project.updatedAt
|
|
4493
|
+
updated_at: project.updatedAt,
|
|
4494
|
+
viewer_role: project.viewerRole
|
|
4438
4495
|
},
|
|
4439
4496
|
active_sessions: sessions.sessions.map((s) => ({
|
|
4440
4497
|
id: s.id,
|
|
@@ -4738,7 +4795,9 @@ var listProjectsTool = {
|
|
|
4738
4795
|
status: p.status,
|
|
4739
4796
|
updated_at: p.updatedAt,
|
|
4740
4797
|
// The list is ordered by this, newest first.
|
|
4741
|
-
last_activity_at: p.lastActivityAt
|
|
4798
|
+
last_activity_at: p.lastActivityAt,
|
|
4799
|
+
// read | write | admin; absent from a platform without project roles.
|
|
4800
|
+
viewer_role: p.viewerRole
|
|
4742
4801
|
})),
|
|
4743
4802
|
total: result.total,
|
|
4744
4803
|
limit: result.limit,
|
|
@@ -7182,11 +7241,29 @@ function wrapToolHandler(tool, ctx) {
|
|
|
7182
7241
|
if (err instanceof NeedsLoginError) {
|
|
7183
7242
|
return errorResult(err.message);
|
|
7184
7243
|
}
|
|
7244
|
+
if (isProjectPermissionDeniedError(err)) {
|
|
7245
|
+
return errorResult(`${tool.name} failed: ${permissionDeniedMessage(err)}`);
|
|
7246
|
+
}
|
|
7185
7247
|
const message = err instanceof Error ? err.message : String(err);
|
|
7186
7248
|
return errorResult(`${tool.name} failed: ${message}`);
|
|
7187
7249
|
}
|
|
7188
7250
|
};
|
|
7189
7251
|
}
|
|
7252
|
+
function permissionDeniedMessage(err) {
|
|
7253
|
+
let required;
|
|
7254
|
+
let viewer;
|
|
7255
|
+
if (err instanceof PlatformPermissionError) {
|
|
7256
|
+
required = err.requiredRole;
|
|
7257
|
+
viewer = err.viewerRole;
|
|
7258
|
+
} else if (err instanceof Error) {
|
|
7259
|
+
const details = parseHttpErrorDetails(err.message);
|
|
7260
|
+
required = details?.required_role;
|
|
7261
|
+
viewer = details?.viewer_role;
|
|
7262
|
+
}
|
|
7263
|
+
const need = typeof required === "string" ? `the ${required} role` : "a higher role";
|
|
7264
|
+
const have = typeof viewer === "string" ? `; you have ${viewer}` : "";
|
|
7265
|
+
return `permission denied \u2014 this action needs ${need} on the project${have}. Ask a project admin for access.`;
|
|
7266
|
+
}
|
|
7190
7267
|
function registerTool(server, tool, ctx) {
|
|
7191
7268
|
server.registerTool(
|
|
7192
7269
|
tool.name,
|
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.161",
|
|
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": {
|