@myspec/mcp-server 1.1.1-next.158 → 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.
Files changed (3) hide show
  1. package/README.md +4 -2
  2. package/dist/index.js +88 -12
  3. 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
 
@@ -2892,11 +2945,7 @@ var HttpSpecSessionClient = class {
2892
2945
  if (opts?.query) params.set("q", opts.query);
2893
2946
  if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
2894
2947
  if (opts?.offset !== void 0) params.set("offset", String(opts.offset));
2895
- const response = await this.httpClient.get(
2896
- `/project/v1/projects/${projectId}/sessions?${params.toString()}`,
2897
- jwtToken,
2898
- {}
2899
- );
2948
+ const response = await this.httpClient.get(`/project/v1/projects/${projectId}/sessions?${params.toString()}`, jwtToken, {});
2900
2949
  return {
2901
2950
  sessions: response.sessions.map(transformSpecSessionResponse),
2902
2951
  total: response.total,
@@ -2904,13 +2953,14 @@ var HttpSpecSessionClient = class {
2904
2953
  offset: response.offset
2905
2954
  };
2906
2955
  }
2907
- async updateSpecSession(sessionId, updates, jwtToken) {
2956
+ async updateSpecSession(sessionId, updates, jwtToken, options) {
2908
2957
  const body = {};
2909
2958
  if (updates.status !== void 0) body.status = updates.status;
2910
2959
  if (updates.messageCount !== void 0) body.message_count = updates.messageCount;
2911
2960
  if (updates.tokenCount !== void 0) body.token_count = updates.tokenCount;
2912
2961
  if (updates.context !== void 0) body.context = updates.context;
2913
2962
  if (updates.chatHistory !== void 0) body.chat_history = updates.chatHistory;
2963
+ if (options?.originClientId) body.origin_client_id = options.originClientId;
2914
2964
  const raw = await this.httpClient.put(
2915
2965
  `/project/v1/sessions/${sessionId}`,
2916
2966
  body,
@@ -2919,8 +2969,10 @@ var HttpSpecSessionClient = class {
2919
2969
  );
2920
2970
  return transformSpecSessionResponse(raw);
2921
2971
  }
2922
- async appendSessionMessage(sessionId, message, jwtToken) {
2923
- const response = await this.httpClient.post(`/project/v1/sessions/${sessionId}/messages`, { message }, jwtToken, {});
2972
+ async appendSessionMessage(sessionId, message, jwtToken, options) {
2973
+ const body = { message };
2974
+ if (options?.originClientId) body.origin_client_id = options.originClientId;
2975
+ const response = await this.httpClient.post(`/project/v1/sessions/${sessionId}/messages`, body, jwtToken, {});
2924
2976
  return { messageCount: response.message_count };
2925
2977
  }
2926
2978
  async archiveSpecSession(sessionId, jwtToken) {
@@ -4095,6 +4147,9 @@ var deleteProjectTool = {
4095
4147
  try {
4096
4148
  await ctx.client.deleteProject(args.project_id);
4097
4149
  } catch (err) {
4150
+ if (isProjectPermissionDeniedError(err)) {
4151
+ throw err;
4152
+ }
4098
4153
  const message = err instanceof Error ? err.message : String(err);
4099
4154
  return errorResult(`Failed to delete project ${args.project_id}: ${message}`);
4100
4155
  }
@@ -4435,7 +4490,8 @@ var getProjectTool = {
4435
4490
  description: project.description,
4436
4491
  status: project.status,
4437
4492
  created_at: project.createdAt,
4438
- updated_at: project.updatedAt
4493
+ updated_at: project.updatedAt,
4494
+ viewer_role: project.viewerRole
4439
4495
  },
4440
4496
  active_sessions: sessions.sessions.map((s) => ({
4441
4497
  id: s.id,
@@ -4739,7 +4795,9 @@ var listProjectsTool = {
4739
4795
  status: p.status,
4740
4796
  updated_at: p.updatedAt,
4741
4797
  // The list is ordered by this, newest first.
4742
- 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
4743
4801
  })),
4744
4802
  total: result.total,
4745
4803
  limit: result.limit,
@@ -7183,11 +7241,29 @@ function wrapToolHandler(tool, ctx) {
7183
7241
  if (err instanceof NeedsLoginError) {
7184
7242
  return errorResult(err.message);
7185
7243
  }
7244
+ if (isProjectPermissionDeniedError(err)) {
7245
+ return errorResult(`${tool.name} failed: ${permissionDeniedMessage(err)}`);
7246
+ }
7186
7247
  const message = err instanceof Error ? err.message : String(err);
7187
7248
  return errorResult(`${tool.name} failed: ${message}`);
7188
7249
  }
7189
7250
  };
7190
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
+ }
7191
7267
  function registerTool(server, tool, ctx) {
7192
7268
  server.registerTool(
7193
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.158",
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": {