@myspec/mcp-server 0.1.2-next.36 → 0.1.2-next.38

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 (2) hide show
  1. package/dist/index.js +151 -43
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -99,7 +99,7 @@ function applyCorsHeaders(res) {
99
99
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
100
100
  }
101
101
  async function waitForOAuthCallback(opts) {
102
- return new Promise((resolve2, reject) => {
102
+ return new Promise((resolve3, reject) => {
103
103
  const settledFlag = { value: false };
104
104
  const timeoutRef = { handle: void 0 };
105
105
  const rlRef = { handle: void 0 };
@@ -157,7 +157,7 @@ async function waitForOAuthCallback(opts) {
157
157
  res.setHeader("Content-Type", "text/html");
158
158
  res.end(SUCCESS_HTML);
159
159
  settle(() => {
160
- resolve2({ code });
160
+ resolve3({ code });
161
161
  });
162
162
  });
163
163
  server.on("error", (err) => {
@@ -204,7 +204,7 @@ async function waitForOAuthCallback(opts) {
204
204
  }
205
205
  settle(() => {
206
206
  opts.logger("Captured pasted token; finishing sign-in...");
207
- resolve2({ code });
207
+ resolve3({ code });
208
208
  });
209
209
  });
210
210
  }
@@ -348,10 +348,10 @@ function createFileCredentialsStore(paths = {}) {
348
348
  async function loadOauthCreds(target) {
349
349
  try {
350
350
  if (process.platform !== "win32") {
351
- const stat = await fs.stat(target);
352
- if ((stat.mode & 63) !== 0) {
351
+ const stat2 = await fs.stat(target);
352
+ if ((stat2.mode & 63) !== 0) {
353
353
  throw new Error(
354
- `Refusing to load credentials from ${target}: file mode is too permissive (${(stat.mode & 511).toString(8)}). Run \`chmod 600 ${target}\` and retry.`
354
+ `Refusing to load credentials from ${target}: file mode is too permissive (${(stat2.mode & 511).toString(8)}). Run \`chmod 600 ${target}\` and retry.`
355
355
  );
356
356
  }
357
357
  }
@@ -840,7 +840,7 @@ function calculateDelay(attempt, config) {
840
840
  return Math.round(cappedDelay * jitterMultiplier);
841
841
  }
842
842
  function sleep(ms) {
843
- return new Promise((resolve2) => setTimeout(resolve2, ms));
843
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
844
844
  }
845
845
  async function withRetry(fn, config = DEFAULT_RETRY_CONFIG, shouldRetry = defaultShouldRetry) {
846
846
  const startTime = Date.now();
@@ -1139,6 +1139,7 @@ function transformSpecFileResponse(raw) {
1139
1139
  revisionCount: raw.revision_count,
1140
1140
  createdAt: new Date(raw.created_at),
1141
1141
  updatedAt: new Date(raw.updated_at),
1142
+ trashedAt: raw.trashed_at ? new Date(raw.trashed_at) : void 0,
1142
1143
  deletedAt: raw.deleted_at ? new Date(raw.deleted_at) : void 0
1143
1144
  };
1144
1145
  }
@@ -1475,18 +1476,49 @@ var HttpFileClient = class {
1475
1476
  if (opts?.offset !== void 0) queryParams.set("offset", String(opts.offset));
1476
1477
  if (opts?.hideArchivedIncomplete)
1477
1478
  queryParams.set("hide_archived_incomplete", "true");
1479
+ if (opts?.includeTrashed) queryParams.set("include_trashed", "true");
1478
1480
  const query = queryParams.toString();
1479
1481
  const path4 = `/project/v1/files?${query}`;
1480
1482
  const raw = await this.httpClient.get(path4, jwtToken, {});
1481
1483
  return transformFileListResult(raw);
1482
1484
  }
1483
- async deleteFile(fileId, jwtToken) {
1485
+ async trashFile(fileId, jwtToken) {
1484
1486
  await this.httpClient.delete(
1485
1487
  `/project/v1/files/${fileId}`,
1486
1488
  jwtToken,
1487
1489
  {}
1488
1490
  );
1489
1491
  }
1492
+ async restoreFile(fileId, jwtToken) {
1493
+ await this.httpClient.post(
1494
+ `/project/v1/files/${fileId}/restore`,
1495
+ {},
1496
+ jwtToken,
1497
+ {}
1498
+ );
1499
+ }
1500
+ async deleteFilePermanently(fileId, jwtToken) {
1501
+ await this.httpClient.delete(
1502
+ `/project/v1/files/${fileId}/permanent`,
1503
+ jwtToken,
1504
+ {}
1505
+ );
1506
+ }
1507
+ async listTrashedFiles(projectId, jwtToken) {
1508
+ const raw = await this.httpClient.get(
1509
+ `/project/v1/projects/${projectId}/files/trashed`,
1510
+ jwtToken,
1511
+ {}
1512
+ );
1513
+ return (raw.files ?? []).map(transformSpecFileResponse);
1514
+ }
1515
+ /**
1516
+ * @deprecated Alias of {@link trashFile}. Moves the file to the Trash Bin
1517
+ * (recoverable); for a permanent delete use {@link deleteFilePermanently}.
1518
+ */
1519
+ async deleteFile(fileId, jwtToken) {
1520
+ await this.trashFile(fileId, jwtToken);
1521
+ }
1490
1522
  async deleteRevision(fileId, revisionNumber, jwtToken) {
1491
1523
  const response = await this.httpClient.delete(
1492
1524
  `/project/v1/files/${fileId}/revisions/${revisionNumber}`,
@@ -2448,6 +2480,59 @@ var updateProjectTool = {
2448
2480
  // src/server/tools/update_spec_file.ts
2449
2481
  import { z as z11 } from "zod";
2450
2482
 
2483
+ // src/server/tools/spec-content-source.ts
2484
+ import { readFile, stat } from "fs/promises";
2485
+ import { isAbsolute, resolve } from "path";
2486
+ var MAX_LOCAL_SPEC_FILE_BYTES = 5 * 1024 * 1024;
2487
+ async function resolveSpecContent(input, maxBytes = MAX_LOCAL_SPEC_FILE_BYTES) {
2488
+ const { content, localFilePath } = input;
2489
+ if (content !== void 0 && localFilePath !== void 0) {
2490
+ return { error: "Provide either content or local_file_path, not both." };
2491
+ }
2492
+ if (content !== void 0) {
2493
+ return { bytes: new TextEncoder().encode(content) };
2494
+ }
2495
+ if (localFilePath === void 0) {
2496
+ return { error: "Provide the file body via content or local_file_path (one is required)." };
2497
+ }
2498
+ const absPath = isAbsolute(localFilePath) ? localFilePath : resolve(process.cwd(), localFilePath);
2499
+ let info;
2500
+ try {
2501
+ info = await stat(absPath);
2502
+ } catch (err) {
2503
+ const code = err.code;
2504
+ if (code === "ENOENT") {
2505
+ return { error: `local file not found: ${absPath}` };
2506
+ }
2507
+ const message = err instanceof Error ? err.message : String(err);
2508
+ return { error: `failed to read local file ${absPath}: ${message}` };
2509
+ }
2510
+ if (!info.isFile()) {
2511
+ return { error: `local_file_path is not a regular file: ${absPath}` };
2512
+ }
2513
+ if (info.size === 0) {
2514
+ return { error: `local file is empty: ${absPath}` };
2515
+ }
2516
+ if (info.size > maxBytes) {
2517
+ return {
2518
+ error: `local file is too large (${String(info.size)} bytes); max is ${String(maxBytes)} bytes.`
2519
+ };
2520
+ }
2521
+ let raw;
2522
+ try {
2523
+ raw = await readFile(absPath);
2524
+ } catch (err) {
2525
+ const message = err instanceof Error ? err.message : String(err);
2526
+ return { error: `failed to read local file ${absPath}: ${message}` };
2527
+ }
2528
+ if (raw.byteLength > maxBytes) {
2529
+ return {
2530
+ error: `local file is too large (${String(raw.byteLength)} bytes); max is ${String(maxBytes)} bytes.`
2531
+ };
2532
+ }
2533
+ return { bytes: new Uint8Array(raw) };
2534
+ }
2535
+
2451
2536
  // src/server/tools/spec-file-output.ts
2452
2537
  function specFileSummary(file) {
2453
2538
  return {
@@ -2469,13 +2554,25 @@ var inputSchema11 = {
2469
2554
  file_id: z11.string().min(1).optional().describe("UUID of the spec file to overwrite. Provide this or (project_id + file_path)."),
2470
2555
  project_id: z11.string().min(1).optional().describe("UUID of the project. Required (with file_path) when file_id is not given."),
2471
2556
  file_path: z11.string().min(1).optional().describe("Path of the spec file. Required (with project_id) when file_id is not given."),
2472
- content: z11.string().min(1).describe("Full UTF-8 replacement content (non-empty), saved as a new revision.")
2557
+ content: z11.string().min(1).optional().describe(
2558
+ "Full UTF-8 replacement content, saved as a new revision. Provide this OR local_file_path (exactly one). Use for in-memory / generated content."
2559
+ ),
2560
+ local_file_path: z11.string().min(1).optional().describe(
2561
+ "Path to a local file whose bytes become the new revision; the server reads it directly. Provide this OR content (exactly one). Absolute paths are most reliable; a relative path resolves against the MCP server working directory. Point this only at an intended spec file \u2014 its bytes are uploaded to the project as-is; never use it for secrets or unrelated files."
2562
+ )
2473
2563
  };
2474
2564
  var updateSpecFileTool = {
2475
2565
  name: "update_spec_file",
2476
- description: "Overwrite an existing spec file by saving a new revision from full content. Reference the file by file_id or by (project_id + file_path).",
2566
+ description: "Overwrite an existing spec file by saving a new revision. Provide the new body inline via `content` or by pointing at a local file with `local_file_path` \u2014 exactly one is required. Prefer `local_file_path` when the body is already an existing local file (the server reads it directly); use `content` for text generated in-memory. Reference the file by file_id or by (project_id + file_path).",
2477
2567
  inputSchema: inputSchema11,
2478
2568
  handler: async (args, ctx) => {
2569
+ const resolved = await resolveSpecContent({
2570
+ content: args.content,
2571
+ localFilePath: args.local_file_path
2572
+ });
2573
+ if (resolved.error || !resolved.bytes) {
2574
+ return errorResult(resolved.error ?? "Failed to resolve file content.");
2575
+ }
2479
2576
  let fileId;
2480
2577
  if (args.file_id) {
2481
2578
  const meta = await ctx.client.getFileMetadata(args.file_id).catch(() => null);
@@ -2494,16 +2591,15 @@ var updateSpecFileTool = {
2494
2591
  } else {
2495
2592
  return errorResult("Provide either file_id, or both project_id and file_path.");
2496
2593
  }
2497
- const content = new TextEncoder().encode(args.content);
2498
- const file = await ctx.client.saveSpecFileRevision(fileId, content);
2594
+ const file = await ctx.client.saveSpecFileRevision(fileId, resolved.bytes);
2499
2595
  return jsonResult({ file: specFileSummary(file) });
2500
2596
  }
2501
2597
  };
2502
2598
 
2503
2599
  // src/server/tools/upload_attachment.ts
2504
2600
  import { createHash as createHash2 } from "crypto";
2505
- import { lstat, readFile } from "fs/promises";
2506
- import { basename, extname, isAbsolute } from "path";
2601
+ import { lstat, readFile as readFile2 } from "fs/promises";
2602
+ import { basename, extname, isAbsolute as isAbsolute2 } from "path";
2507
2603
  import { z as z12 } from "zod";
2508
2604
  var DEFAULT_MIME = "application/octet-stream";
2509
2605
  var MAX_ATTACHMENT_MB = MAX_ATTACHMENT_BYTES / (1024 * 1024);
@@ -2622,7 +2718,7 @@ var uploadAttachmentTool = {
2622
2718
  description: `Upload a local file as a new attachment on a project. Reads \`file_path\` from the host running the MCP server, computes its SHA256, then performs the two-phase platform upload. Max ${String(MAX_ATTACHMENT_MB)} MB. Supported content: PDF, DOCX, XLSX, and any UTF-8 text file (XML, JSON, YAML, HTML, Markdown, CSV, source code, ...). Binary files outside the structured types will be rejected by the platform.`,
2623
2719
  inputSchema: inputSchema12,
2624
2720
  handler: async (args, ctx) => {
2625
- if (!isAbsolute(args.file_path)) {
2721
+ if (!isAbsolute2(args.file_path)) {
2626
2722
  return errorResult(`file_path must be an absolute path; got "${args.file_path}".`);
2627
2723
  }
2628
2724
  let stats;
@@ -2658,7 +2754,7 @@ var uploadAttachmentTool = {
2658
2754
  }
2659
2755
  let buf;
2660
2756
  try {
2661
- buf = await readFile(args.file_path);
2757
+ buf = await readFile2(args.file_path);
2662
2758
  } catch (err) {
2663
2759
  const code = err.code;
2664
2760
  if (code === "EACCES" || code === "EPERM") {
@@ -2762,15 +2858,16 @@ function validateSpecFilePath(filePath) {
2762
2858
  if (segments.some((s) => s === "" || s === ".")) {
2763
2859
  return 'file_path must be a normalized path (no empty, "." or trailing-slash segments).';
2764
2860
  }
2765
- if (segments[0] !== "specs") {
2766
- return "file_path must be rooted at 'specs' (e.g. specs/changes/add-oauth-login/proposal.md).";
2861
+ const root = segments[0];
2862
+ if (root !== "specs" && root !== "openspec") {
2863
+ return "file_path must be rooted at 'specs' or 'openspec' (e.g. specs/changes/add-oauth-login/proposal.md).";
2767
2864
  }
2768
2865
  if (segments.length < 2) {
2769
- return "file_path must include a filename under 'specs' (e.g. specs/proposal.md).";
2866
+ return `file_path must include a filename under '${root}' (e.g. ${root}/proposal.md).`;
2770
2867
  }
2771
2868
  const dirLevels = segments.length - 2;
2772
2869
  if (dirLevels > MAX_SPEC_DIR_DEPTH) {
2773
- return `file_path must not nest deeper than ${String(MAX_SPEC_DIR_DEPTH)} directory levels below specs.`;
2870
+ return `file_path must not nest deeper than ${String(MAX_SPEC_DIR_DEPTH)} directory levels below the '${root}' root.`;
2774
2871
  }
2775
2872
  return null;
2776
2873
  }
@@ -2799,20 +2896,32 @@ var SPEC_FILE_TYPES = [
2799
2896
  var inputSchema13 = {
2800
2897
  project_id: z13.string().min(1).describe("UUID of the project to upload the file into."),
2801
2898
  file_path: z13.string().min(1).describe(
2802
- 'Path of the new spec file, rooted at "specs" with at most 3 directory levels below specs (e.g. specs/changes/add-oauth-login/proposal.md).'
2899
+ 'Path of the new spec file, rooted at "specs" or "openspec" with at most 3 directory levels below the root (e.g. specs/changes/add-oauth-login/proposal.md).'
2900
+ ),
2901
+ content: z13.string().min(1).optional().describe(
2902
+ "Full UTF-8 text content of the spec file. Provide this OR local_file_path (exactly one). Use for in-memory / generated content."
2903
+ ),
2904
+ local_file_path: z13.string().min(1).optional().describe(
2905
+ "Path to a local file whose bytes become the spec file body; the server reads it directly. Provide this OR content (exactly one). Absolute paths are most reliable; a relative path resolves against the MCP server working directory. Point this only at an intended spec file \u2014 its bytes are uploaded to the project as-is; never use it for secrets or unrelated files."
2803
2906
  ),
2804
- content: z13.string().min(1).describe("Full UTF-8 text content of the spec file (non-empty)."),
2805
2907
  file_type: z13.enum(SPEC_FILE_TYPES).optional().describe("Spec file type. Omit to derive from the filename (e.g. proposal.md \u2192 proposal).")
2806
2908
  };
2807
2909
  var uploadSpecFileTool = {
2808
2910
  name: "upload_spec_file",
2809
- description: "Upload a new, session-less spec file into a project from full content. Rejects if a file already exists at the same path (use update_spec_file to overwrite).",
2911
+ description: "Upload a new, session-less spec file into a project. Provide the file body inline via `content` or by pointing at a local file with `local_file_path` \u2014 exactly one is required. Prefer `local_file_path` when the body is already an existing local file (the server reads it directly, avoiding sending large content through the model); use `content` for text generated in-memory. Rejects if a file already exists at the same path (use update_spec_file to overwrite).",
2810
2912
  inputSchema: inputSchema13,
2811
2913
  handler: async (args, ctx) => {
2812
2914
  const pathError = validateSpecFilePath(args.file_path);
2813
2915
  if (pathError) {
2814
2916
  return errorResult(pathError);
2815
2917
  }
2918
+ const resolved = await resolveSpecContent({
2919
+ content: args.content,
2920
+ localFilePath: args.local_file_path
2921
+ });
2922
+ if (resolved.error || !resolved.bytes) {
2923
+ return errorResult(resolved.error ?? "Failed to resolve file content.");
2924
+ }
2816
2925
  const existing = await ctx.client.findSpecFileByPath(args.project_id, args.file_path);
2817
2926
  if (existing) {
2818
2927
  return errorResult(
@@ -2820,12 +2929,11 @@ var uploadSpecFileTool = {
2820
2929
  );
2821
2930
  }
2822
2931
  const fileType = args.file_type ?? deriveFileType(args.file_path);
2823
- const content = new TextEncoder().encode(args.content);
2824
2932
  const file = await ctx.client.uploadSpecFile({
2825
2933
  projectId: args.project_id,
2826
2934
  filePath: args.file_path,
2827
2935
  fileType,
2828
- content
2936
+ content: resolved.bytes
2829
2937
  });
2830
2938
  return jsonResult({ file: specFileSummary(file) });
2831
2939
  }
@@ -2917,13 +3025,13 @@ var WebSocketClientTransport = class {
2917
3025
  return Promise.resolve();
2918
3026
  }
2919
3027
  send(message) {
2920
- return new Promise((resolve2, reject) => {
3028
+ return new Promise((resolve3, reject) => {
2921
3029
  this.ws.send(JSON.stringify(message), (err) => {
2922
3030
  if (err) {
2923
3031
  reject(err);
2924
3032
  return;
2925
3033
  }
2926
- resolve2();
3034
+ resolve3();
2927
3035
  });
2928
3036
  });
2929
3037
  }
@@ -2957,7 +3065,7 @@ var WebSocketClientTransport = class {
2957
3065
 
2958
3066
  // src/reverse/safe-path.ts
2959
3067
  import { realpath } from "fs/promises";
2960
- import { isAbsolute as isAbsolute2, resolve, sep } from "path";
3068
+ import { isAbsolute as isAbsolute3, resolve as resolve2, sep } from "path";
2961
3069
  var PathOutsideRootError = class extends Error {
2962
3070
  constructor(userPath) {
2963
3071
  super(`path_outside_root: ${userPath}`);
@@ -2975,10 +3083,10 @@ var AbsolutePathError = class extends Error {
2975
3083
  userPath;
2976
3084
  };
2977
3085
  async function resolveSafePath(realRoot, userPath, opts = {}) {
2978
- if (isAbsolute2(userPath)) {
3086
+ if (isAbsolute3(userPath)) {
2979
3087
  throw new AbsolutePathError(userPath);
2980
3088
  }
2981
- const joined = resolve(realRoot, userPath);
3089
+ const joined = resolve2(realRoot, userPath);
2982
3090
  let resolved;
2983
3091
  try {
2984
3092
  resolved = await realpath(joined);
@@ -3083,7 +3191,7 @@ function ripgrepIgnoreGlobs() {
3083
3191
 
3084
3192
  // src/reverse/pack-codebase.ts
3085
3193
  import { createHash as createHash3 } from "crypto";
3086
- import { mkdtemp, readFile as readFile2, rm } from "fs/promises";
3194
+ import { mkdtemp, readFile as readFile3, rm } from "fs/promises";
3087
3195
  import { tmpdir } from "os";
3088
3196
  import { join } from "path";
3089
3197
  import { runCli } from "repomix";
@@ -3220,7 +3328,7 @@ async function invokeRepomix(exec, targetDir, args, timeoutMs) {
3220
3328
  const execPromise = exec(["."], targetDir, options);
3221
3329
  try {
3222
3330
  await withTimeout(execPromise, timeoutMs);
3223
- const content = await readFile2(outputFile, "utf-8");
3331
+ const content = await readFile3(outputFile, "utf-8");
3224
3332
  await rm(workDir, { recursive: true, force: true });
3225
3333
  return content;
3226
3334
  } catch (err) {
@@ -3370,20 +3478,20 @@ async function handleRead(args, opts) {
3370
3478
  return structuredError("path_ignored", `Path matches ignore rule ${check.rule ?? ""}`);
3371
3479
  }
3372
3480
  }
3373
- let stat;
3481
+ let stat2;
3374
3482
  try {
3375
- stat = await fs4.stat(realPath);
3483
+ stat2 = await fs4.stat(realPath);
3376
3484
  } catch (err) {
3377
3485
  return structuredError("stat_failed", errorMessage(err));
3378
3486
  }
3379
- if (!stat.isFile()) {
3487
+ if (!stat2.isFile()) {
3380
3488
  return structuredError("not_a_file", `Not a regular file: ${args.path}`);
3381
3489
  }
3382
- if (stat.size > READ_FILE_MAX_BYTES) {
3490
+ if (stat2.size > READ_FILE_MAX_BYTES) {
3383
3491
  return structuredError(
3384
3492
  "file_too_large",
3385
- `File is ${String(stat.size)} bytes; the read cap is ${String(READ_FILE_MAX_BYTES)} bytes. Use local_fs_grep to find the lines you need, or local_pack_codebase to pull the whole tree.`,
3386
- { sizeBytes: stat.size, maxBytes: READ_FILE_MAX_BYTES }
3493
+ `File is ${String(stat2.size)} bytes; the read cap is ${String(READ_FILE_MAX_BYTES)} bytes. Use local_fs_grep to find the lines you need, or local_pack_codebase to pull the whole tree.`,
3494
+ { sizeBytes: stat2.size, maxBytes: READ_FILE_MAX_BYTES }
3387
3495
  );
3388
3496
  }
3389
3497
  let raw;
@@ -3988,7 +4096,7 @@ async function runOneConnection(opts) {
3988
4096
  pongTimer = null;
3989
4097
  }
3990
4098
  });
3991
- return new Promise((resolve2) => {
4099
+ return new Promise((resolve3) => {
3992
4100
  ws.once("close", (code, reasonBuf) => {
3993
4101
  clearInterval(pingTimer);
3994
4102
  if (pongTimer) clearTimeout(pongTimer);
@@ -3998,7 +4106,7 @@ async function runOneConnection(opts) {
3998
4106
  `
3999
4107
  );
4000
4108
  const terminal = TERMINAL_CLOSE_CODES.has(code);
4001
- resolve2({
4109
+ resolve3({
4002
4110
  terminal,
4003
4111
  reason: `code=${String(code)} ${reasonStr}`,
4004
4112
  upMs: Date.now() - connectedAt,
@@ -4008,10 +4116,10 @@ async function runOneConnection(opts) {
4008
4116
  });
4009
4117
  }
4010
4118
  function waitForOpen(ws) {
4011
- return new Promise((resolve2, reject) => {
4119
+ return new Promise((resolve3, reject) => {
4012
4120
  const onOpen = () => {
4013
4121
  ws.removeListener("error", onError);
4014
- resolve2();
4122
+ resolve3();
4015
4123
  };
4016
4124
  const onError = (err) => {
4017
4125
  ws.removeListener("open", onOpen);
@@ -4022,7 +4130,7 @@ function waitForOpen(ws) {
4022
4130
  });
4023
4131
  }
4024
4132
  function sleep2(ms) {
4025
- return new Promise((resolve2) => setTimeout(resolve2, ms));
4133
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
4026
4134
  }
4027
4135
 
4028
4136
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myspec/mcp-server",
3
- "version": "0.1.2-next.36",
3
+ "version": "0.1.2-next.38",
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": {