@myspec/mcp-server 0.1.2-next.34 → 0.1.2-next.35

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 +417 -70
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -818,6 +818,9 @@ var TokenManager = class {
818
818
  var RefreshRejectedError = class extends NeedsLoginError {
819
819
  };
820
820
 
821
+ // src/platform/client.ts
822
+ import { createHash } from "crypto";
823
+
821
824
  // ../../packages/platform-client/shared/types.ts
822
825
  var RETRYABLE_STATUS_CODES = [408, 429, 502, 503, 504];
823
826
 
@@ -1126,7 +1129,7 @@ function transformSpecFileResponse(raw) {
1126
1129
  return {
1127
1130
  id: raw.id,
1128
1131
  projectId: raw.project_id,
1129
- sessionId: raw.session_id,
1132
+ sessionId: raw.session_id ?? null,
1130
1133
  org: raw.org,
1131
1134
  fileType: normalizeFileType(raw.file_type),
1132
1135
  filePath: raw.file_path,
@@ -1337,12 +1340,14 @@ var HttpFileClient = class {
1337
1340
  async initiateUpload(request, jwtToken) {
1338
1341
  const body = {
1339
1342
  project_id: request.projectId,
1340
- session_id: request.sessionId,
1341
1343
  file_type: request.fileType,
1342
1344
  file_path: request.filePath,
1343
1345
  file_size_bytes: request.fileSizeBytes,
1344
1346
  checksum: request.checksum
1345
1347
  };
1348
+ if (request.sessionId) {
1349
+ body.session_id = request.sessionId;
1350
+ }
1346
1351
  const response = await this.httpClient.post(
1347
1352
  "/project/v1/files/initiate",
1348
1353
  body,
@@ -1830,6 +1835,82 @@ var PlatformClient = class {
1830
1835
  fileUri: uploaded.fileUri
1831
1836
  };
1832
1837
  }
1838
+ /** Creates a new project. */
1839
+ async createProject(request) {
1840
+ return this.withTokenRetry((jwt, c) => c.project.createProject(request, jwt));
1841
+ }
1842
+ /** Updates an existing project's metadata (name, description, status, ...). */
1843
+ async updateProject(projectId, updates) {
1844
+ return this.withTokenRetry((jwt, c) => c.project.updateProject(projectId, updates, jwt));
1845
+ }
1846
+ /** Archives a project by setting its status to `archived`. */
1847
+ async archiveProject(projectId) {
1848
+ return this.updateProject(projectId, { status: "archived" });
1849
+ }
1850
+ /** Unarchives a project by setting its status to `active`. */
1851
+ async unarchiveProject(projectId) {
1852
+ return this.updateProject(projectId, { status: "active" });
1853
+ }
1854
+ /**
1855
+ * Soft-deletes a project. The platform rejects this unless the project is
1856
+ * already archived, surfacing a validation/conflict error the caller reports.
1857
+ */
1858
+ async deleteProject(projectId) {
1859
+ await this.withTokenRetry((jwt, c) => c.project.deleteProject(projectId, jwt));
1860
+ }
1861
+ /**
1862
+ * Two-phase upload of a new, session-less ("unbound") spec file. Reserves a
1863
+ * file id (Phase 1, `sessionId` omitted so the file is unbound), then PUTs
1864
+ * the content (Phase 2). Size and a `sha256:`-prefixed checksum are computed
1865
+ * here from the bytes. Returns the created file's metadata.
1866
+ */
1867
+ async uploadSpecFile(args) {
1868
+ const initiated = await this.withTokenRetry(
1869
+ (jwt, c) => c.file.initiateUpload(
1870
+ {
1871
+ projectId: args.projectId,
1872
+ filePath: args.filePath,
1873
+ fileType: args.fileType,
1874
+ fileSizeBytes: args.content.byteLength,
1875
+ checksum: sha256Prefixed(args.content)
1876
+ },
1877
+ jwt
1878
+ )
1879
+ );
1880
+ await this.withTokenRetry(
1881
+ (jwt, c) => c.file.uploadContent(initiated.fileId, args.content, jwt)
1882
+ );
1883
+ return this.getFileMetadata(initiated.fileId);
1884
+ }
1885
+ /**
1886
+ * Saves a new revision (overwrite) of an existing spec file by id. The
1887
+ * platform computes the checksum server-side. Returns the file's updated
1888
+ * metadata (including the bumped revision count).
1889
+ */
1890
+ async saveSpecFileRevision(fileId, content) {
1891
+ await this.withTokenRetry((jwt, c) => c.file.saveManualRevision(fileId, content, jwt));
1892
+ return this.getFileMetadata(fileId);
1893
+ }
1894
+ /**
1895
+ * Finds a project's spec file by exact file_path, paging through the list.
1896
+ * Returns null when no file matches. Used to enforce create-vs-overwrite
1897
+ * semantics and to resolve a file reference by path.
1898
+ */
1899
+ async findSpecFileByPath(projectId, filePath) {
1900
+ const pageSize = 100;
1901
+ let offset = 0;
1902
+ for (; ; ) {
1903
+ const page = await this.listFiles(projectId, { limit: pageSize, offset });
1904
+ const found = page.files.find((f) => f.filePath === filePath);
1905
+ if (found) {
1906
+ return found;
1907
+ }
1908
+ offset += page.files.length;
1909
+ if (page.files.length < pageSize || offset >= page.total) {
1910
+ return null;
1911
+ }
1912
+ }
1913
+ }
1833
1914
  /**
1834
1915
  * Builds the HTTP clients on first use, resolving the base URL lazily. A
1835
1916
  * successful build is cached for the process lifetime; a failed resolution
@@ -1866,6 +1947,9 @@ var PlatformClient = class {
1866
1947
  }
1867
1948
  }
1868
1949
  };
1950
+ function sha256Prefixed(content) {
1951
+ return "sha256:" + createHash("sha256").update(content).digest("hex");
1952
+ }
1869
1953
  function statusOf(err) {
1870
1954
  if (typeof err !== "object" || err === null) {
1871
1955
  return void 0;
@@ -1892,16 +1976,91 @@ function errorResult(message) {
1892
1976
  return { content: [{ type: "text", text: message }], isError: true };
1893
1977
  }
1894
1978
 
1895
- // src/server/tools/get_attachment.ts
1979
+ // src/server/tools/archive_project.ts
1896
1980
  import { z } from "zod";
1981
+
1982
+ // src/server/tools/project-output.ts
1983
+ function projectSummary(project) {
1984
+ return {
1985
+ id: project.id,
1986
+ name: project.name,
1987
+ description: project.description,
1988
+ status: project.status,
1989
+ project_type: project.projectType,
1990
+ created_at: project.createdAt,
1991
+ updated_at: project.updatedAt
1992
+ };
1993
+ }
1994
+
1995
+ // src/server/tools/archive_project.ts
1897
1996
  var inputSchema = {
1898
- attachment_id: z.string().min(1).describe("UUID of the attachment"),
1899
- include_download_url: z.number().int().nonnegative().optional().describe("Set to any positive value to include a signed download URL. Omit or 0 to skip.")
1997
+ project_id: z.string().min(1).describe("UUID of the project to archive.")
1998
+ };
1999
+ var archiveProjectTool = {
2000
+ name: "archive_project",
2001
+ description: 'Archive a project (sets status to "archived"). Idempotent: archiving an already-archived project succeeds.',
2002
+ inputSchema,
2003
+ handler: async (args, ctx) => {
2004
+ const project = await ctx.client.archiveProject(args.project_id);
2005
+ return jsonResult({ project: projectSummary(project) });
2006
+ }
2007
+ };
2008
+
2009
+ // src/server/tools/create_project.ts
2010
+ import { z as z2 } from "zod";
2011
+ var inputSchema2 = {
2012
+ name: z2.string().describe("Name of the project to create."),
2013
+ description: z2.string().optional().describe("Optional project description.")
2014
+ };
2015
+ var createProjectTool = {
2016
+ name: "create_project",
2017
+ description: "Create a new project. Returns the created project id and metadata.",
2018
+ inputSchema: inputSchema2,
2019
+ handler: async (args, ctx) => {
2020
+ const name = args.name.trim();
2021
+ if (name === "") {
2022
+ return errorResult("name is required and cannot be empty.");
2023
+ }
2024
+ const project = await ctx.client.createProject({
2025
+ name,
2026
+ description: args.description
2027
+ });
2028
+ return jsonResult({ project: projectSummary(project) });
2029
+ }
2030
+ };
2031
+
2032
+ // src/server/tools/delete_project.ts
2033
+ import { z as z3 } from "zod";
2034
+ var inputSchema3 = {
2035
+ project_id: z3.string().min(1).describe("UUID of the project to delete.")
2036
+ };
2037
+ var deleteProjectTool = {
2038
+ name: "delete_project",
2039
+ description: "Soft-delete a project. The project must be archived first; deleting a non-archived project is rejected.",
2040
+ inputSchema: inputSchema3,
2041
+ handler: async (args, ctx) => {
2042
+ try {
2043
+ await ctx.client.deleteProject(args.project_id);
2044
+ } catch (err) {
2045
+ const message = err instanceof Error ? err.message : String(err);
2046
+ return errorResult(
2047
+ `Failed to delete project ${args.project_id}: ${message} (a project must be archived before it can be deleted).`
2048
+ );
2049
+ }
2050
+ return jsonResult({ deleted: true, project_id: args.project_id });
2051
+ }
2052
+ };
2053
+
2054
+ // src/server/tools/get_attachment.ts
2055
+ import { z as z4 } from "zod";
2056
+ var inputSchema4 = {
2057
+ attachment_id: z4.string().min(1).describe("UUID of the attachment"),
2058
+ include_download_url: z4.number().int().nonnegative().optional().describe("Set to any positive value to include a signed download URL. Omit or 0 to skip.")
1900
2059
  };
1901
2060
  var getAttachmentTool = {
1902
2061
  name: "get_attachment",
1903
2062
  description: "Get attachment metadata. With `include_download_url>0`, also returns a signed `download_url`.",
1904
- inputSchema,
2063
+ inputSchema: inputSchema4,
1905
2064
  handler: async (args, ctx) => {
1906
2065
  const attachment = await ctx.client.getAttachmentMetadata(args.attachment_id);
1907
2066
  if (!attachment) {
@@ -1930,17 +2089,17 @@ var getAttachmentTool = {
1930
2089
  };
1931
2090
 
1932
2091
  // src/server/tools/get_file.ts
1933
- import { z as z2 } from "zod";
1934
- var inputSchema2 = {
1935
- file_id: z2.string().min(1).describe("UUID of the file"),
1936
- include_download_url: z2.number().int().nonnegative().optional().describe(
2092
+ import { z as z5 } from "zod";
2093
+ var inputSchema5 = {
2094
+ file_id: z5.string().min(1).describe("UUID of the file"),
2095
+ include_download_url: z5.number().int().nonnegative().optional().describe(
1937
2096
  "Revision number to include a download URL for. Returns a signed, time-limited URL for that revision (latest or historical) that needs no credentials. Omit or 0 to skip."
1938
2097
  )
1939
2098
  };
1940
2099
  var getFileTool = {
1941
2100
  name: "get_file",
1942
2101
  description: "Get file metadata. With `include_download_url=N`, also returns a signed, time-limited download URL for revision N (latest or historical). The URL is self-authenticating, so no credentials are returned.",
1943
- inputSchema: inputSchema2,
2102
+ inputSchema: inputSchema5,
1944
2103
  handler: async (args, ctx) => {
1945
2104
  const meta = await ctx.client.getFileMetadata(args.file_id);
1946
2105
  const base = {
@@ -1976,14 +2135,14 @@ var getFileTool = {
1976
2135
  };
1977
2136
 
1978
2137
  // src/server/tools/get_project.ts
1979
- import { z as z3 } from "zod";
1980
- var inputSchema3 = {
1981
- project_id: z3.string().min(1).describe("UUID of the project to fetch")
2138
+ import { z as z6 } from "zod";
2139
+ var inputSchema6 = {
2140
+ project_id: z6.string().min(1).describe("UUID of the project to fetch")
1982
2141
  };
1983
2142
  var getProjectTool = {
1984
2143
  name: "get_project",
1985
2144
  description: "Get details for a project including all active spec sessions, files (id + metadata), and attachments (id + metadata).",
1986
- inputSchema: inputSchema3,
2145
+ inputSchema: inputSchema6,
1987
2146
  handler: async (args, ctx) => {
1988
2147
  const [project, sessions, files, attachments] = await Promise.all([
1989
2148
  ctx.client.getProject(args.project_id),
@@ -2029,18 +2188,18 @@ var getProjectTool = {
2029
2188
  };
2030
2189
 
2031
2190
  // src/server/tools/list_projects.ts
2032
- import { z as z4 } from "zod";
2033
- var inputSchema4 = {
2034
- query: z4.string().optional().describe(
2191
+ import { z as z7 } from "zod";
2192
+ var inputSchema7 = {
2193
+ query: z7.string().optional().describe(
2035
2194
  "Search term to filter projects by name. Omit to list all projects."
2036
2195
  ),
2037
- limit: z4.number().int().positive().max(100).optional().describe("Max projects to return (default 20)"),
2038
- offset: z4.number().int().nonnegative().optional().describe("Pagination offset (default 0)")
2196
+ limit: z7.number().int().positive().max(100).optional().describe("Max projects to return (default 20)"),
2197
+ offset: z7.number().int().nonnegative().optional().describe("Pagination offset (default 0)")
2039
2198
  };
2040
2199
  var listProjectsTool = {
2041
2200
  name: "list_projects",
2042
2201
  description: "List or search MySpec projects accessible to the authenticated user.",
2043
- inputSchema: inputSchema4,
2202
+ inputSchema: inputSchema7,
2044
2203
  handler: async (args, ctx) => {
2045
2204
  const result = await ctx.client.listProjects({
2046
2205
  query: args.query,
@@ -2066,7 +2225,7 @@ var listProjectsTool = {
2066
2225
  import { promises as fs3 } from "fs";
2067
2226
  import os3 from "os";
2068
2227
  import path3 from "path";
2069
- import { z as z5 } from "zod";
2228
+ import { z as z8 } from "zod";
2070
2229
 
2071
2230
  // src/server/download-path.ts
2072
2231
  import { promises as fs2 } from "fs";
@@ -2133,14 +2292,14 @@ async function writeBytesAtomically(target, bytes) {
2133
2292
 
2134
2293
  // src/server/tools/read_file.ts
2135
2294
  var MAX_READ_BYTES = 1 * 1024 * 1024;
2136
- var inputSchema5 = {
2137
- file_id: z5.string().min(1).describe("UUID of the file"),
2138
- revision: z5.number().int().positive().optional().describe("Specific revision number to read (defaults to the latest revision).")
2295
+ var inputSchema8 = {
2296
+ file_id: z8.string().min(1).describe("UUID of the file"),
2297
+ revision: z8.number().int().positive().optional().describe("Specific revision number to read (defaults to the latest revision).")
2139
2298
  };
2140
2299
  var readFileTool = {
2141
2300
  name: "read_file",
2142
2301
  description: "Return a file's UTF-8 content. Bytes are cached on disk under the configured cache root (default ~/.myspec) at `project/<projectId>/file/<fileId>/rev/<rev>/<basename>` and reused on subsequent reads of the same revision. Fails for binary files or files larger than 1 MiB \u2014 use get_file with `include_download_url` to obtain a download URL for those.",
2143
- inputSchema: inputSchema5,
2302
+ inputSchema: inputSchema8,
2144
2303
  handler: async (args, ctx) => {
2145
2304
  const meta = await ctx.client.getFileMetadata(args.file_id);
2146
2305
  const targetRevision = args.revision ?? meta.revisionCount;
@@ -2225,11 +2384,104 @@ function isBinary(bytes) {
2225
2384
  return false;
2226
2385
  }
2227
2386
 
2387
+ // src/server/tools/unarchive_project.ts
2388
+ import { z as z9 } from "zod";
2389
+ var inputSchema9 = {
2390
+ project_id: z9.string().min(1).describe("UUID of the project to unarchive.")
2391
+ };
2392
+ var unarchiveProjectTool = {
2393
+ name: "unarchive_project",
2394
+ description: 'Unarchive a project (sets status to "active"). Idempotent: unarchiving an already-active project succeeds.',
2395
+ inputSchema: inputSchema9,
2396
+ handler: async (args, ctx) => {
2397
+ const project = await ctx.client.unarchiveProject(args.project_id);
2398
+ return jsonResult({ project: projectSummary(project) });
2399
+ }
2400
+ };
2401
+
2402
+ // src/server/tools/update_project.ts
2403
+ import { z as z10 } from "zod";
2404
+ var inputSchema10 = {
2405
+ project_id: z10.string().min(1).describe("UUID of the project to update."),
2406
+ name: z10.string().optional().describe("New project name."),
2407
+ description: z10.string().optional().describe("New project description.")
2408
+ };
2409
+ var updateProjectTool = {
2410
+ name: "update_project",
2411
+ description: "Update a project's metadata (name, description). Returns the updated metadata.",
2412
+ inputSchema: inputSchema10,
2413
+ handler: async (args, ctx) => {
2414
+ if (args.name === void 0 && args.description === void 0) {
2415
+ return errorResult("Provide at least one field to update (name or description).");
2416
+ }
2417
+ const project = await ctx.client.updateProject(args.project_id, {
2418
+ name: args.name,
2419
+ description: args.description
2420
+ });
2421
+ return jsonResult({ project: projectSummary(project) });
2422
+ }
2423
+ };
2424
+
2425
+ // src/server/tools/update_spec_file.ts
2426
+ import { z as z11 } from "zod";
2427
+
2428
+ // src/server/tools/spec-file-output.ts
2429
+ function specFileSummary(file) {
2430
+ return {
2431
+ id: file.id,
2432
+ project_id: file.projectId,
2433
+ session_id: file.sessionId,
2434
+ file_path: file.filePath,
2435
+ file_type: file.fileType,
2436
+ file_size_bytes: file.fileSizeBytes,
2437
+ checksum: file.checksum,
2438
+ revision_count: file.revisionCount,
2439
+ created_at: file.createdAt,
2440
+ updated_at: file.updatedAt
2441
+ };
2442
+ }
2443
+
2444
+ // src/server/tools/update_spec_file.ts
2445
+ var inputSchema11 = {
2446
+ file_id: z11.string().min(1).optional().describe("UUID of the spec file to overwrite. Provide this or (project_id + file_path)."),
2447
+ project_id: z11.string().min(1).optional().describe("UUID of the project. Required (with file_path) when file_id is not given."),
2448
+ file_path: z11.string().min(1).optional().describe("Path of the spec file. Required (with project_id) when file_id is not given."),
2449
+ content: z11.string().min(1).describe("Full UTF-8 replacement content (non-empty), saved as a new revision.")
2450
+ };
2451
+ var updateSpecFileTool = {
2452
+ name: "update_spec_file",
2453
+ 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).",
2454
+ inputSchema: inputSchema11,
2455
+ handler: async (args, ctx) => {
2456
+ let fileId;
2457
+ if (args.file_id) {
2458
+ const meta = await ctx.client.getFileMetadata(args.file_id).catch(() => null);
2459
+ if (!meta) {
2460
+ return errorResult(`No spec file found with id ${args.file_id}.`);
2461
+ }
2462
+ fileId = meta.id;
2463
+ } else if (args.project_id && args.file_path) {
2464
+ const match = await ctx.client.findSpecFileByPath(args.project_id, args.file_path);
2465
+ if (!match) {
2466
+ return errorResult(
2467
+ `No spec file found at ${args.file_path} in project ${args.project_id}.`
2468
+ );
2469
+ }
2470
+ fileId = match.id;
2471
+ } else {
2472
+ return errorResult("Provide either file_id, or both project_id and file_path.");
2473
+ }
2474
+ const content = new TextEncoder().encode(args.content);
2475
+ const file = await ctx.client.saveSpecFileRevision(fileId, content);
2476
+ return jsonResult({ file: specFileSummary(file) });
2477
+ }
2478
+ };
2479
+
2228
2480
  // src/server/tools/upload_attachment.ts
2229
- import { createHash } from "crypto";
2481
+ import { createHash as createHash2 } from "crypto";
2230
2482
  import { lstat, readFile } from "fs/promises";
2231
2483
  import { basename, extname, isAbsolute } from "path";
2232
- import { z as z6 } from "zod";
2484
+ import { z as z12 } from "zod";
2233
2485
  var DEFAULT_MIME = "application/octet-stream";
2234
2486
  var MAX_ATTACHMENT_MB = MAX_ATTACHMENT_BYTES / (1024 * 1024);
2235
2487
  var STRUCTURED_EXTENSION_MIME = {
@@ -2329,23 +2581,23 @@ function inferMimeFromName(name) {
2329
2581
  }
2330
2582
  return DEFAULT_MIME;
2331
2583
  }
2332
- var inputSchema6 = {
2333
- project_id: z6.string().min(1).describe("UUID of the project to attach the file to."),
2334
- file_path: z6.string().min(1).describe(
2584
+ var inputSchema12 = {
2585
+ project_id: z12.string().min(1).describe("UUID of the project to attach the file to."),
2586
+ file_path: z12.string().min(1).describe(
2335
2587
  "Absolute path to the local file to upload. The MCP server reads this path from its own host."
2336
2588
  ),
2337
- file_name: z6.string().min(1).optional().describe("Override the filename recorded on the attachment. Defaults to the path basename."),
2338
- mime_type: z6.string().min(1).optional().describe(
2589
+ file_name: z12.string().min(1).optional().describe("Override the filename recorded on the attachment. Defaults to the path basename."),
2590
+ mime_type: z12.string().min(1).optional().describe(
2339
2591
  "Override the MIME type sent to the platform. The platform re-detects from content, so this is only a hint. If omitted, inferred from the file extension \u2014 structured (.pdf, .docx, .xlsx) keep their canonical MIME; common UTF-8 text formats (.xml, .json, .yaml, .html, .csv, .md, source code, ...) are sent as text/plain."
2340
2592
  ),
2341
- override: z6.boolean().optional().describe(
2593
+ override: z12.boolean().optional().describe(
2342
2594
  "When true, any existing (non-deleted) attachment on the project whose recorded filename exactly matches `file_name` (or the path basename) is soft-deleted before the new upload, so the new attachment keeps the original name instead of getting an auto-dedup `(1)` suffix. The platform has no in-place content-replace API, so the new upload always gets a new `attachment_id`; any replaced ids are returned as `overridden_attachment_ids` (array, possibly empty) so callers can refresh stored references. Defaults to false (let the platform auto-dedup)."
2343
2595
  )
2344
2596
  };
2345
2597
  var uploadAttachmentTool = {
2346
2598
  name: "upload_attachment",
2347
2599
  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.`,
2348
- inputSchema: inputSchema6,
2600
+ inputSchema: inputSchema12,
2349
2601
  handler: async (args, ctx) => {
2350
2602
  if (!isAbsolute(args.file_path)) {
2351
2603
  return errorResult(`file_path must be an absolute path; got "${args.file_path}".`);
@@ -2402,7 +2654,7 @@ var uploadAttachmentTool = {
2402
2654
  }
2403
2655
  const fileName = args.file_name ?? basename(args.file_path);
2404
2656
  const mimeType = args.mime_type ?? inferMimeFromName(fileName);
2405
- const checksum = `sha256:${createHash("sha256").update(buf).digest("hex")}`;
2657
+ const checksum = `sha256:${createHash2("sha256").update(buf).digest("hex")}`;
2406
2658
  const bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
2407
2659
  const existing = await ctx.client.listAttachments(args.project_id);
2408
2660
  const nameMatches = existing.filter((a) => a.name === fileName);
@@ -2468,6 +2720,94 @@ var uploadAttachmentTool = {
2468
2720
  }
2469
2721
  };
2470
2722
 
2723
+ // src/server/tools/upload_spec_file.ts
2724
+ import { z as z13 } from "zod";
2725
+
2726
+ // src/server/spec-path.ts
2727
+ var MAX_SPEC_DIR_DEPTH = 3;
2728
+ function validateSpecFilePath(filePath) {
2729
+ if (!filePath) {
2730
+ return "file_path is required.";
2731
+ }
2732
+ if (filePath.startsWith("/")) {
2733
+ return "file_path must be a relative path, not an absolute path.";
2734
+ }
2735
+ const segments = filePath.split("/");
2736
+ if (segments.some((s) => s === "..")) {
2737
+ return "file_path must not contain '..' (parent directory) segments.";
2738
+ }
2739
+ if (segments.some((s) => s === "" || s === ".")) {
2740
+ return 'file_path must be a normalized path (no empty, "." or trailing-slash segments).';
2741
+ }
2742
+ if (segments[0] !== "specs") {
2743
+ return "file_path must be rooted at 'specs' (e.g. specs/changes/add-oauth-login/proposal.md).";
2744
+ }
2745
+ if (segments.length < 2) {
2746
+ return "file_path must include a filename under 'specs' (e.g. specs/proposal.md).";
2747
+ }
2748
+ const dirLevels = segments.length - 2;
2749
+ if (dirLevels > MAX_SPEC_DIR_DEPTH) {
2750
+ return `file_path must not nest deeper than ${String(MAX_SPEC_DIR_DEPTH)} directory levels below specs.`;
2751
+ }
2752
+ return null;
2753
+ }
2754
+ var FILE_TYPE_BY_BASENAME = {
2755
+ constitution: "constitution",
2756
+ requirements: "requirements",
2757
+ solution: "solution",
2758
+ tasks: "tasks",
2759
+ proposal: "proposal"
2760
+ };
2761
+ function deriveFileType(filePath) {
2762
+ const filename = filePath.split("/").pop() ?? "";
2763
+ const base = filename.replace(/\.[^.]+$/, "").toLowerCase();
2764
+ return FILE_TYPE_BY_BASENAME[base] ?? "openspec-spec";
2765
+ }
2766
+
2767
+ // src/server/tools/upload_spec_file.ts
2768
+ var SPEC_FILE_TYPES = [
2769
+ "constitution",
2770
+ "requirements",
2771
+ "solution",
2772
+ "tasks",
2773
+ "proposal",
2774
+ "openspec-spec"
2775
+ ];
2776
+ var inputSchema13 = {
2777
+ project_id: z13.string().min(1).describe("UUID of the project to upload the file into."),
2778
+ file_path: z13.string().min(1).describe(
2779
+ '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).'
2780
+ ),
2781
+ content: z13.string().min(1).describe("Full UTF-8 text content of the spec file (non-empty)."),
2782
+ file_type: z13.enum(SPEC_FILE_TYPES).optional().describe("Spec file type. Omit to derive from the filename (e.g. proposal.md \u2192 proposal).")
2783
+ };
2784
+ var uploadSpecFileTool = {
2785
+ name: "upload_spec_file",
2786
+ 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).",
2787
+ inputSchema: inputSchema13,
2788
+ handler: async (args, ctx) => {
2789
+ const pathError = validateSpecFilePath(args.file_path);
2790
+ if (pathError) {
2791
+ return errorResult(pathError);
2792
+ }
2793
+ const existing = await ctx.client.findSpecFileByPath(args.project_id, args.file_path);
2794
+ if (existing) {
2795
+ return errorResult(
2796
+ `A spec file already exists at ${args.file_path} in project ${args.project_id}. Use update_spec_file to overwrite it.`
2797
+ );
2798
+ }
2799
+ const fileType = args.file_type ?? deriveFileType(args.file_path);
2800
+ const content = new TextEncoder().encode(args.content);
2801
+ const file = await ctx.client.uploadSpecFile({
2802
+ projectId: args.project_id,
2803
+ filePath: args.file_path,
2804
+ fileType,
2805
+ content
2806
+ });
2807
+ return jsonResult({ file: specFileSummary(file) });
2808
+ }
2809
+ };
2810
+
2471
2811
  // src/server/server.ts
2472
2812
  function buildServer(deps) {
2473
2813
  const server = new McpServer({
@@ -2480,6 +2820,13 @@ function buildServer(deps) {
2480
2820
  registerTool(server, readFileTool, deps.client);
2481
2821
  registerTool(server, getAttachmentTool, deps.client);
2482
2822
  registerTool(server, uploadAttachmentTool, deps.client);
2823
+ registerTool(server, createProjectTool, deps.client);
2824
+ registerTool(server, updateProjectTool, deps.client);
2825
+ registerTool(server, archiveProjectTool, deps.client);
2826
+ registerTool(server, unarchiveProjectTool, deps.client);
2827
+ registerTool(server, deleteProjectTool, deps.client);
2828
+ registerTool(server, uploadSpecFileTool, deps.client);
2829
+ registerTool(server, updateSpecFileTool, deps.client);
2483
2830
  return server;
2484
2831
  }
2485
2832
  async function startStdioServer(deps) {
@@ -2633,7 +2980,7 @@ async function canonicaliseRoot(root) {
2633
2980
  import { promises as fs4 } from "fs";
2634
2981
  import { join as join2, relative } from "path";
2635
2982
  import { spawn } from "child_process";
2636
- import { z as z7 } from "zod";
2983
+ import { z as z14 } from "zod";
2637
2984
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
2638
2985
 
2639
2986
  // src/reverse/ignore.ts
@@ -2712,7 +3059,7 @@ function ripgrepIgnoreGlobs() {
2712
3059
  }
2713
3060
 
2714
3061
  // src/reverse/pack-codebase.ts
2715
- import { createHash as createHash2 } from "crypto";
3062
+ import { createHash as createHash3 } from "crypto";
2716
3063
  import { mkdtemp, readFile as readFile2, rm } from "fs/promises";
2717
3064
  import { tmpdir } from "os";
2718
3065
  import { join } from "path";
@@ -2783,7 +3130,7 @@ function computeOutputId(inputs) {
2783
3130
  i: inputs.includePatterns ?? "",
2784
3131
  x: inputs.ignorePatterns ?? ""
2785
3132
  });
2786
- const digest = createHash2("sha256").update(payload).digest("hex").slice(0, 12);
3133
+ const digest = createHash3("sha256").update(payload).digest("hex").slice(0, 12);
2787
3134
  return `pack-${digest}`;
2788
3135
  }
2789
3136
  function readPage(cache, outputId, page) {
@@ -2883,37 +3230,37 @@ var READ_FILE_MAX_LINES = 2e3;
2883
3230
  var LIST_DIR_HARD_CAP = 5e3;
2884
3231
  var LIST_DIR_DEFAULT_CAP = 1e3;
2885
3232
  var GREP_MAX_MATCHES_DEFAULT = 200;
2886
- var listDirInput = z7.object({
2887
- path: z7.string(),
2888
- recursive: z7.boolean().optional(),
2889
- maxEntries: z7.number().int().positive().optional(),
2890
- includeIgnored: z7.boolean().optional()
3233
+ var listDirInput = z14.object({
3234
+ path: z14.string(),
3235
+ recursive: z14.boolean().optional(),
3236
+ maxEntries: z14.number().int().positive().optional(),
3237
+ includeIgnored: z14.boolean().optional()
2891
3238
  });
2892
- var readFileInput = z7.object({
2893
- path: z7.string(),
2894
- offset: z7.number().int().nonnegative().optional(),
2895
- limit: z7.number().int().positive().optional(),
2896
- includeIgnored: z7.boolean().optional()
3239
+ var readFileInput = z14.object({
3240
+ path: z14.string(),
3241
+ offset: z14.number().int().nonnegative().optional(),
3242
+ limit: z14.number().int().positive().optional(),
3243
+ includeIgnored: z14.boolean().optional()
2897
3244
  });
2898
- var grepInput = z7.object({
2899
- pattern: z7.string().describe(
3245
+ var grepInput = z14.object({
3246
+ pattern: z14.string().describe(
2900
3247
  "Search pattern. Interpreted as a regular expression by default (ripgrep syntax), e.g. `foo|bar` or `[Rr]ole`. Set `isRegex: false` to match the pattern as an exact literal string instead."
2901
3248
  ),
2902
- path: z7.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
2903
- isRegex: z7.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
2904
- caseSensitive: z7.boolean().optional(),
2905
- maxMatches: z7.number().int().positive().optional(),
2906
- contextLines: z7.number().int().nonnegative().optional(),
2907
- includeIgnored: z7.boolean().optional()
3249
+ path: z14.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
3250
+ isRegex: z14.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
3251
+ caseSensitive: z14.boolean().optional(),
3252
+ maxMatches: z14.number().int().positive().optional(),
3253
+ contextLines: z14.number().int().nonnegative().optional(),
3254
+ includeIgnored: z14.boolean().optional()
2908
3255
  });
2909
- var packCodebaseInput = z7.object({
2910
- subpath: z7.string().optional(),
2911
- includePatterns: z7.string().optional(),
2912
- ignorePatterns: z7.string().optional()
3256
+ var packCodebaseInput = z14.object({
3257
+ subpath: z14.string().optional(),
3258
+ includePatterns: z14.string().optional(),
3259
+ ignorePatterns: z14.string().optional()
2913
3260
  });
2914
- var packCodebaseReadPageInput = z7.object({
2915
- outputId: z7.string(),
2916
- page: z7.number().int().positive()
3261
+ var packCodebaseReadPageInput = z14.object({
3262
+ outputId: z14.string(),
3263
+ page: z14.number().int().positive()
2917
3264
  });
2918
3265
  function structuredError(error, message, extra) {
2919
3266
  const payload = { error, message, ...extra ?? {} };
@@ -3441,7 +3788,7 @@ function jsonSchemaFromZod(schema) {
3441
3788
  return zodToJson(schema);
3442
3789
  }
3443
3790
  function zodToJson(schema) {
3444
- if (schema instanceof z7.ZodObject) {
3791
+ if (schema instanceof z14.ZodObject) {
3445
3792
  const shape = schema.shape;
3446
3793
  const properties = {};
3447
3794
  const required = [];
@@ -3456,19 +3803,19 @@ function zodToJson(schema) {
3456
3803
  if (required.length > 0) out.required = required;
3457
3804
  return out;
3458
3805
  }
3459
- if (schema instanceof z7.ZodOptional) {
3806
+ if (schema instanceof z14.ZodOptional) {
3460
3807
  return zodToJson(schema.unwrap());
3461
3808
  }
3462
- if (schema instanceof z7.ZodString) {
3809
+ if (schema instanceof z14.ZodString) {
3463
3810
  return { type: "string" };
3464
3811
  }
3465
- if (schema instanceof z7.ZodNumber) {
3812
+ if (schema instanceof z14.ZodNumber) {
3466
3813
  return { type: "number" };
3467
3814
  }
3468
- if (schema instanceof z7.ZodBoolean) {
3815
+ if (schema instanceof z14.ZodBoolean) {
3469
3816
  return { type: "boolean" };
3470
3817
  }
3471
- if (schema instanceof z7.ZodEnum) {
3818
+ if (schema instanceof z14.ZodEnum) {
3472
3819
  return { type: "string", enum: schema.options };
3473
3820
  }
3474
3821
  return { type: "string" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myspec/mcp-server",
3
- "version": "0.1.2-next.34",
3
+ "version": "0.1.2-next.35",
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": {