@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.
- package/README.md +4 -2
- package/dist/index.js +88 -12
- 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
|
|
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.
|
|
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": {
|