@myspec/mcp-server 1.1.1-next.159 → 1.1.1-next.160

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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.159",
3
+ "version": "1.1.1-next.160",
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": {