@myspec/mcp-server 0.1.2-next.37 → 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 +118 -42
  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();
@@ -2480,6 +2480,59 @@ var updateProjectTool = {
2480
2480
  // src/server/tools/update_spec_file.ts
2481
2481
  import { z as z11 } from "zod";
2482
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
+
2483
2536
  // src/server/tools/spec-file-output.ts
2484
2537
  function specFileSummary(file) {
2485
2538
  return {
@@ -2501,13 +2554,25 @@ var inputSchema11 = {
2501
2554
  file_id: z11.string().min(1).optional().describe("UUID of the spec file to overwrite. Provide this or (project_id + file_path)."),
2502
2555
  project_id: z11.string().min(1).optional().describe("UUID of the project. Required (with file_path) when file_id is not given."),
2503
2556
  file_path: z11.string().min(1).optional().describe("Path of the spec file. Required (with project_id) when file_id is not given."),
2504
- 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
+ )
2505
2563
  };
2506
2564
  var updateSpecFileTool = {
2507
2565
  name: "update_spec_file",
2508
- 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).",
2509
2567
  inputSchema: inputSchema11,
2510
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
+ }
2511
2576
  let fileId;
2512
2577
  if (args.file_id) {
2513
2578
  const meta = await ctx.client.getFileMetadata(args.file_id).catch(() => null);
@@ -2526,16 +2591,15 @@ var updateSpecFileTool = {
2526
2591
  } else {
2527
2592
  return errorResult("Provide either file_id, or both project_id and file_path.");
2528
2593
  }
2529
- const content = new TextEncoder().encode(args.content);
2530
- const file = await ctx.client.saveSpecFileRevision(fileId, content);
2594
+ const file = await ctx.client.saveSpecFileRevision(fileId, resolved.bytes);
2531
2595
  return jsonResult({ file: specFileSummary(file) });
2532
2596
  }
2533
2597
  };
2534
2598
 
2535
2599
  // src/server/tools/upload_attachment.ts
2536
2600
  import { createHash as createHash2 } from "crypto";
2537
- import { lstat, readFile } from "fs/promises";
2538
- import { basename, extname, isAbsolute } from "path";
2601
+ import { lstat, readFile as readFile2 } from "fs/promises";
2602
+ import { basename, extname, isAbsolute as isAbsolute2 } from "path";
2539
2603
  import { z as z12 } from "zod";
2540
2604
  var DEFAULT_MIME = "application/octet-stream";
2541
2605
  var MAX_ATTACHMENT_MB = MAX_ATTACHMENT_BYTES / (1024 * 1024);
@@ -2654,7 +2718,7 @@ var uploadAttachmentTool = {
2654
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.`,
2655
2719
  inputSchema: inputSchema12,
2656
2720
  handler: async (args, ctx) => {
2657
- if (!isAbsolute(args.file_path)) {
2721
+ if (!isAbsolute2(args.file_path)) {
2658
2722
  return errorResult(`file_path must be an absolute path; got "${args.file_path}".`);
2659
2723
  }
2660
2724
  let stats;
@@ -2690,7 +2754,7 @@ var uploadAttachmentTool = {
2690
2754
  }
2691
2755
  let buf;
2692
2756
  try {
2693
- buf = await readFile(args.file_path);
2757
+ buf = await readFile2(args.file_path);
2694
2758
  } catch (err) {
2695
2759
  const code = err.code;
2696
2760
  if (code === "EACCES" || code === "EPERM") {
@@ -2794,15 +2858,16 @@ function validateSpecFilePath(filePath) {
2794
2858
  if (segments.some((s) => s === "" || s === ".")) {
2795
2859
  return 'file_path must be a normalized path (no empty, "." or trailing-slash segments).';
2796
2860
  }
2797
- if (segments[0] !== "specs") {
2798
- 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).";
2799
2864
  }
2800
2865
  if (segments.length < 2) {
2801
- 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).`;
2802
2867
  }
2803
2868
  const dirLevels = segments.length - 2;
2804
2869
  if (dirLevels > MAX_SPEC_DIR_DEPTH) {
2805
- 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.`;
2806
2871
  }
2807
2872
  return null;
2808
2873
  }
@@ -2831,20 +2896,32 @@ var SPEC_FILE_TYPES = [
2831
2896
  var inputSchema13 = {
2832
2897
  project_id: z13.string().min(1).describe("UUID of the project to upload the file into."),
2833
2898
  file_path: z13.string().min(1).describe(
2834
- '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."
2835
2906
  ),
2836
- content: z13.string().min(1).describe("Full UTF-8 text content of the spec file (non-empty)."),
2837
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).")
2838
2908
  };
2839
2909
  var uploadSpecFileTool = {
2840
2910
  name: "upload_spec_file",
2841
- 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).",
2842
2912
  inputSchema: inputSchema13,
2843
2913
  handler: async (args, ctx) => {
2844
2914
  const pathError = validateSpecFilePath(args.file_path);
2845
2915
  if (pathError) {
2846
2916
  return errorResult(pathError);
2847
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
+ }
2848
2925
  const existing = await ctx.client.findSpecFileByPath(args.project_id, args.file_path);
2849
2926
  if (existing) {
2850
2927
  return errorResult(
@@ -2852,12 +2929,11 @@ var uploadSpecFileTool = {
2852
2929
  );
2853
2930
  }
2854
2931
  const fileType = args.file_type ?? deriveFileType(args.file_path);
2855
- const content = new TextEncoder().encode(args.content);
2856
2932
  const file = await ctx.client.uploadSpecFile({
2857
2933
  projectId: args.project_id,
2858
2934
  filePath: args.file_path,
2859
2935
  fileType,
2860
- content
2936
+ content: resolved.bytes
2861
2937
  });
2862
2938
  return jsonResult({ file: specFileSummary(file) });
2863
2939
  }
@@ -2949,13 +3025,13 @@ var WebSocketClientTransport = class {
2949
3025
  return Promise.resolve();
2950
3026
  }
2951
3027
  send(message) {
2952
- return new Promise((resolve2, reject) => {
3028
+ return new Promise((resolve3, reject) => {
2953
3029
  this.ws.send(JSON.stringify(message), (err) => {
2954
3030
  if (err) {
2955
3031
  reject(err);
2956
3032
  return;
2957
3033
  }
2958
- resolve2();
3034
+ resolve3();
2959
3035
  });
2960
3036
  });
2961
3037
  }
@@ -2989,7 +3065,7 @@ var WebSocketClientTransport = class {
2989
3065
 
2990
3066
  // src/reverse/safe-path.ts
2991
3067
  import { realpath } from "fs/promises";
2992
- import { isAbsolute as isAbsolute2, resolve, sep } from "path";
3068
+ import { isAbsolute as isAbsolute3, resolve as resolve2, sep } from "path";
2993
3069
  var PathOutsideRootError = class extends Error {
2994
3070
  constructor(userPath) {
2995
3071
  super(`path_outside_root: ${userPath}`);
@@ -3007,10 +3083,10 @@ var AbsolutePathError = class extends Error {
3007
3083
  userPath;
3008
3084
  };
3009
3085
  async function resolveSafePath(realRoot, userPath, opts = {}) {
3010
- if (isAbsolute2(userPath)) {
3086
+ if (isAbsolute3(userPath)) {
3011
3087
  throw new AbsolutePathError(userPath);
3012
3088
  }
3013
- const joined = resolve(realRoot, userPath);
3089
+ const joined = resolve2(realRoot, userPath);
3014
3090
  let resolved;
3015
3091
  try {
3016
3092
  resolved = await realpath(joined);
@@ -3115,7 +3191,7 @@ function ripgrepIgnoreGlobs() {
3115
3191
 
3116
3192
  // src/reverse/pack-codebase.ts
3117
3193
  import { createHash as createHash3 } from "crypto";
3118
- import { mkdtemp, readFile as readFile2, rm } from "fs/promises";
3194
+ import { mkdtemp, readFile as readFile3, rm } from "fs/promises";
3119
3195
  import { tmpdir } from "os";
3120
3196
  import { join } from "path";
3121
3197
  import { runCli } from "repomix";
@@ -3252,7 +3328,7 @@ async function invokeRepomix(exec, targetDir, args, timeoutMs) {
3252
3328
  const execPromise = exec(["."], targetDir, options);
3253
3329
  try {
3254
3330
  await withTimeout(execPromise, timeoutMs);
3255
- const content = await readFile2(outputFile, "utf-8");
3331
+ const content = await readFile3(outputFile, "utf-8");
3256
3332
  await rm(workDir, { recursive: true, force: true });
3257
3333
  return content;
3258
3334
  } catch (err) {
@@ -3402,20 +3478,20 @@ async function handleRead(args, opts) {
3402
3478
  return structuredError("path_ignored", `Path matches ignore rule ${check.rule ?? ""}`);
3403
3479
  }
3404
3480
  }
3405
- let stat;
3481
+ let stat2;
3406
3482
  try {
3407
- stat = await fs4.stat(realPath);
3483
+ stat2 = await fs4.stat(realPath);
3408
3484
  } catch (err) {
3409
3485
  return structuredError("stat_failed", errorMessage(err));
3410
3486
  }
3411
- if (!stat.isFile()) {
3487
+ if (!stat2.isFile()) {
3412
3488
  return structuredError("not_a_file", `Not a regular file: ${args.path}`);
3413
3489
  }
3414
- if (stat.size > READ_FILE_MAX_BYTES) {
3490
+ if (stat2.size > READ_FILE_MAX_BYTES) {
3415
3491
  return structuredError(
3416
3492
  "file_too_large",
3417
- `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.`,
3418
- { 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 }
3419
3495
  );
3420
3496
  }
3421
3497
  let raw;
@@ -4020,7 +4096,7 @@ async function runOneConnection(opts) {
4020
4096
  pongTimer = null;
4021
4097
  }
4022
4098
  });
4023
- return new Promise((resolve2) => {
4099
+ return new Promise((resolve3) => {
4024
4100
  ws.once("close", (code, reasonBuf) => {
4025
4101
  clearInterval(pingTimer);
4026
4102
  if (pongTimer) clearTimeout(pongTimer);
@@ -4030,7 +4106,7 @@ async function runOneConnection(opts) {
4030
4106
  `
4031
4107
  );
4032
4108
  const terminal = TERMINAL_CLOSE_CODES.has(code);
4033
- resolve2({
4109
+ resolve3({
4034
4110
  terminal,
4035
4111
  reason: `code=${String(code)} ${reasonStr}`,
4036
4112
  upMs: Date.now() - connectedAt,
@@ -4040,10 +4116,10 @@ async function runOneConnection(opts) {
4040
4116
  });
4041
4117
  }
4042
4118
  function waitForOpen(ws) {
4043
- return new Promise((resolve2, reject) => {
4119
+ return new Promise((resolve3, reject) => {
4044
4120
  const onOpen = () => {
4045
4121
  ws.removeListener("error", onError);
4046
- resolve2();
4122
+ resolve3();
4047
4123
  };
4048
4124
  const onError = (err) => {
4049
4125
  ws.removeListener("open", onOpen);
@@ -4054,7 +4130,7 @@ function waitForOpen(ws) {
4054
4130
  });
4055
4131
  }
4056
4132
  function sleep2(ms) {
4057
- return new Promise((resolve2) => setTimeout(resolve2, ms));
4133
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
4058
4134
  }
4059
4135
 
4060
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.37",
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": {