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

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 +440 -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,102 @@ 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. Only a project that is already archived can be deleted. If the project is still active, this tool refuses and you must NOT archive it on the user's behalf to make the deletion go through \u2014 instead, reconfirm with the user whether deleting the project is truly intended.",
2040
+ inputSchema: inputSchema3,
2041
+ handler: async (args, ctx) => {
2042
+ let status;
2043
+ try {
2044
+ const project = await ctx.client.getProject(args.project_id);
2045
+ status = project.status;
2046
+ } catch (err) {
2047
+ const message = err instanceof Error ? err.message : String(err);
2048
+ return errorResult(`Failed to load project ${args.project_id}: ${message}`);
2049
+ }
2050
+ if (status !== "archived") {
2051
+ return errorResult(
2052
+ `Project ${args.project_id} (status '${status}') can't be deleted. Reconfirm with the user whether deleting this project is intended or a mistake before taking any further action.`
2053
+ );
2054
+ }
2055
+ try {
2056
+ await ctx.client.deleteProject(args.project_id);
2057
+ } catch (err) {
2058
+ const message = err instanceof Error ? err.message : String(err);
2059
+ return errorResult(`Failed to delete project ${args.project_id}: ${message}`);
2060
+ }
2061
+ return jsonResult({ deleted: true, project_id: args.project_id });
2062
+ }
2063
+ };
2064
+
2065
+ // src/server/tools/get_attachment.ts
2066
+ import { z as z4 } from "zod";
2067
+ var inputSchema4 = {
2068
+ attachment_id: z4.string().min(1).describe("UUID of the attachment"),
2069
+ 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
2070
  };
1901
2071
  var getAttachmentTool = {
1902
2072
  name: "get_attachment",
1903
2073
  description: "Get attachment metadata. With `include_download_url>0`, also returns a signed `download_url`.",
1904
- inputSchema,
2074
+ inputSchema: inputSchema4,
1905
2075
  handler: async (args, ctx) => {
1906
2076
  const attachment = await ctx.client.getAttachmentMetadata(args.attachment_id);
1907
2077
  if (!attachment) {
@@ -1930,17 +2100,17 @@ var getAttachmentTool = {
1930
2100
  };
1931
2101
 
1932
2102
  // 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(
2103
+ import { z as z5 } from "zod";
2104
+ var inputSchema5 = {
2105
+ file_id: z5.string().min(1).describe("UUID of the file"),
2106
+ include_download_url: z5.number().int().nonnegative().optional().describe(
1937
2107
  "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
2108
  )
1939
2109
  };
1940
2110
  var getFileTool = {
1941
2111
  name: "get_file",
1942
2112
  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,
2113
+ inputSchema: inputSchema5,
1944
2114
  handler: async (args, ctx) => {
1945
2115
  const meta = await ctx.client.getFileMetadata(args.file_id);
1946
2116
  const base = {
@@ -1976,14 +2146,14 @@ var getFileTool = {
1976
2146
  };
1977
2147
 
1978
2148
  // 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")
2149
+ import { z as z6 } from "zod";
2150
+ var inputSchema6 = {
2151
+ project_id: z6.string().min(1).describe("UUID of the project to fetch")
1982
2152
  };
1983
2153
  var getProjectTool = {
1984
2154
  name: "get_project",
1985
2155
  description: "Get details for a project including all active spec sessions, files (id + metadata), and attachments (id + metadata).",
1986
- inputSchema: inputSchema3,
2156
+ inputSchema: inputSchema6,
1987
2157
  handler: async (args, ctx) => {
1988
2158
  const [project, sessions, files, attachments] = await Promise.all([
1989
2159
  ctx.client.getProject(args.project_id),
@@ -2029,18 +2199,18 @@ var getProjectTool = {
2029
2199
  };
2030
2200
 
2031
2201
  // src/server/tools/list_projects.ts
2032
- import { z as z4 } from "zod";
2033
- var inputSchema4 = {
2034
- query: z4.string().optional().describe(
2202
+ import { z as z7 } from "zod";
2203
+ var inputSchema7 = {
2204
+ query: z7.string().optional().describe(
2035
2205
  "Search term to filter projects by name. Omit to list all projects."
2036
2206
  ),
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)")
2207
+ limit: z7.number().int().positive().max(100).optional().describe("Max projects to return (default 20)"),
2208
+ offset: z7.number().int().nonnegative().optional().describe("Pagination offset (default 0)")
2039
2209
  };
2040
2210
  var listProjectsTool = {
2041
2211
  name: "list_projects",
2042
2212
  description: "List or search MySpec projects accessible to the authenticated user.",
2043
- inputSchema: inputSchema4,
2213
+ inputSchema: inputSchema7,
2044
2214
  handler: async (args, ctx) => {
2045
2215
  const result = await ctx.client.listProjects({
2046
2216
  query: args.query,
@@ -2066,7 +2236,7 @@ var listProjectsTool = {
2066
2236
  import { promises as fs3 } from "fs";
2067
2237
  import os3 from "os";
2068
2238
  import path3 from "path";
2069
- import { z as z5 } from "zod";
2239
+ import { z as z8 } from "zod";
2070
2240
 
2071
2241
  // src/server/download-path.ts
2072
2242
  import { promises as fs2 } from "fs";
@@ -2133,14 +2303,14 @@ async function writeBytesAtomically(target, bytes) {
2133
2303
 
2134
2304
  // src/server/tools/read_file.ts
2135
2305
  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).")
2306
+ var inputSchema8 = {
2307
+ file_id: z8.string().min(1).describe("UUID of the file"),
2308
+ revision: z8.number().int().positive().optional().describe("Specific revision number to read (defaults to the latest revision).")
2139
2309
  };
2140
2310
  var readFileTool = {
2141
2311
  name: "read_file",
2142
2312
  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,
2313
+ inputSchema: inputSchema8,
2144
2314
  handler: async (args, ctx) => {
2145
2315
  const meta = await ctx.client.getFileMetadata(args.file_id);
2146
2316
  const targetRevision = args.revision ?? meta.revisionCount;
@@ -2225,11 +2395,116 @@ function isBinary(bytes) {
2225
2395
  return false;
2226
2396
  }
2227
2397
 
2398
+ // src/server/tools/unarchive_project.ts
2399
+ import { z as z9 } from "zod";
2400
+ var inputSchema9 = {
2401
+ project_id: z9.string().min(1).describe("UUID of the project to unarchive.")
2402
+ };
2403
+ var unarchiveProjectTool = {
2404
+ name: "unarchive_project",
2405
+ description: 'Unarchive a project (sets status to "active"). Idempotent: unarchiving an already-active project succeeds.',
2406
+ inputSchema: inputSchema9,
2407
+ handler: async (args, ctx) => {
2408
+ const project = await ctx.client.unarchiveProject(args.project_id);
2409
+ return jsonResult({ project: projectSummary(project) });
2410
+ }
2411
+ };
2412
+
2413
+ // src/server/tools/update_project.ts
2414
+ import { z as z10 } from "zod";
2415
+ var inputSchema10 = {
2416
+ project_id: z10.string().min(1).describe("UUID of the project to update."),
2417
+ name: z10.string().min(1).optional().describe("New project name."),
2418
+ description: z10.string().optional().describe("New project description."),
2419
+ project_type: z10.enum(["greenfield", "brownfield", "auto"]).optional().describe(
2420
+ "Codebase type. 'greenfield' or 'brownfield' sets it explicitly; 'auto' clears it so the workflow decides. Omit to leave unchanged."
2421
+ )
2422
+ };
2423
+ var updateProjectTool = {
2424
+ name: "update_project",
2425
+ description: "Update a project's metadata (name, description, project_type). Returns the updated metadata.",
2426
+ inputSchema: inputSchema10,
2427
+ handler: async (args, ctx) => {
2428
+ if (args.name === void 0 && args.description === void 0 && args.project_type === void 0) {
2429
+ return errorResult(
2430
+ "Provide at least one field to update (name, description, or project_type)."
2431
+ );
2432
+ }
2433
+ let projectType;
2434
+ if (args.project_type === "auto") {
2435
+ projectType = "";
2436
+ } else {
2437
+ projectType = args.project_type;
2438
+ }
2439
+ const project = await ctx.client.updateProject(args.project_id, {
2440
+ name: args.name,
2441
+ description: args.description,
2442
+ projectType
2443
+ });
2444
+ return jsonResult({ project: projectSummary(project) });
2445
+ }
2446
+ };
2447
+
2448
+ // src/server/tools/update_spec_file.ts
2449
+ import { z as z11 } from "zod";
2450
+
2451
+ // src/server/tools/spec-file-output.ts
2452
+ function specFileSummary(file) {
2453
+ return {
2454
+ id: file.id,
2455
+ project_id: file.projectId,
2456
+ session_id: file.sessionId,
2457
+ file_path: file.filePath,
2458
+ file_type: file.fileType,
2459
+ file_size_bytes: file.fileSizeBytes,
2460
+ checksum: file.checksum,
2461
+ revision_count: file.revisionCount,
2462
+ created_at: file.createdAt,
2463
+ updated_at: file.updatedAt
2464
+ };
2465
+ }
2466
+
2467
+ // src/server/tools/update_spec_file.ts
2468
+ var inputSchema11 = {
2469
+ file_id: z11.string().min(1).optional().describe("UUID of the spec file to overwrite. Provide this or (project_id + file_path)."),
2470
+ project_id: z11.string().min(1).optional().describe("UUID of the project. Required (with file_path) when file_id is not given."),
2471
+ 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.")
2473
+ };
2474
+ var updateSpecFileTool = {
2475
+ 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).",
2477
+ inputSchema: inputSchema11,
2478
+ handler: async (args, ctx) => {
2479
+ let fileId;
2480
+ if (args.file_id) {
2481
+ const meta = await ctx.client.getFileMetadata(args.file_id).catch(() => null);
2482
+ if (!meta) {
2483
+ return errorResult(`No spec file found with id ${args.file_id}.`);
2484
+ }
2485
+ fileId = meta.id;
2486
+ } else if (args.project_id && args.file_path) {
2487
+ const match = await ctx.client.findSpecFileByPath(args.project_id, args.file_path);
2488
+ if (!match) {
2489
+ return errorResult(
2490
+ `No spec file found at ${args.file_path} in project ${args.project_id}.`
2491
+ );
2492
+ }
2493
+ fileId = match.id;
2494
+ } else {
2495
+ return errorResult("Provide either file_id, or both project_id and file_path.");
2496
+ }
2497
+ const content = new TextEncoder().encode(args.content);
2498
+ const file = await ctx.client.saveSpecFileRevision(fileId, content);
2499
+ return jsonResult({ file: specFileSummary(file) });
2500
+ }
2501
+ };
2502
+
2228
2503
  // src/server/tools/upload_attachment.ts
2229
- import { createHash } from "crypto";
2504
+ import { createHash as createHash2 } from "crypto";
2230
2505
  import { lstat, readFile } from "fs/promises";
2231
2506
  import { basename, extname, isAbsolute } from "path";
2232
- import { z as z6 } from "zod";
2507
+ import { z as z12 } from "zod";
2233
2508
  var DEFAULT_MIME = "application/octet-stream";
2234
2509
  var MAX_ATTACHMENT_MB = MAX_ATTACHMENT_BYTES / (1024 * 1024);
2235
2510
  var STRUCTURED_EXTENSION_MIME = {
@@ -2329,23 +2604,23 @@ function inferMimeFromName(name) {
2329
2604
  }
2330
2605
  return DEFAULT_MIME;
2331
2606
  }
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(
2607
+ var inputSchema12 = {
2608
+ project_id: z12.string().min(1).describe("UUID of the project to attach the file to."),
2609
+ file_path: z12.string().min(1).describe(
2335
2610
  "Absolute path to the local file to upload. The MCP server reads this path from its own host."
2336
2611
  ),
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(
2612
+ file_name: z12.string().min(1).optional().describe("Override the filename recorded on the attachment. Defaults to the path basename."),
2613
+ mime_type: z12.string().min(1).optional().describe(
2339
2614
  "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
2615
  ),
2341
- override: z6.boolean().optional().describe(
2616
+ override: z12.boolean().optional().describe(
2342
2617
  "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
2618
  )
2344
2619
  };
2345
2620
  var uploadAttachmentTool = {
2346
2621
  name: "upload_attachment",
2347
2622
  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,
2623
+ inputSchema: inputSchema12,
2349
2624
  handler: async (args, ctx) => {
2350
2625
  if (!isAbsolute(args.file_path)) {
2351
2626
  return errorResult(`file_path must be an absolute path; got "${args.file_path}".`);
@@ -2402,7 +2677,7 @@ var uploadAttachmentTool = {
2402
2677
  }
2403
2678
  const fileName = args.file_name ?? basename(args.file_path);
2404
2679
  const mimeType = args.mime_type ?? inferMimeFromName(fileName);
2405
- const checksum = `sha256:${createHash("sha256").update(buf).digest("hex")}`;
2680
+ const checksum = `sha256:${createHash2("sha256").update(buf).digest("hex")}`;
2406
2681
  const bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
2407
2682
  const existing = await ctx.client.listAttachments(args.project_id);
2408
2683
  const nameMatches = existing.filter((a) => a.name === fileName);
@@ -2468,6 +2743,94 @@ var uploadAttachmentTool = {
2468
2743
  }
2469
2744
  };
2470
2745
 
2746
+ // src/server/tools/upload_spec_file.ts
2747
+ import { z as z13 } from "zod";
2748
+
2749
+ // src/server/spec-path.ts
2750
+ var MAX_SPEC_DIR_DEPTH = 3;
2751
+ function validateSpecFilePath(filePath) {
2752
+ if (!filePath) {
2753
+ return "file_path is required.";
2754
+ }
2755
+ if (filePath.startsWith("/")) {
2756
+ return "file_path must be a relative path, not an absolute path.";
2757
+ }
2758
+ const segments = filePath.split("/");
2759
+ if (segments.some((s) => s === "..")) {
2760
+ return "file_path must not contain '..' (parent directory) segments.";
2761
+ }
2762
+ if (segments.some((s) => s === "" || s === ".")) {
2763
+ return 'file_path must be a normalized path (no empty, "." or trailing-slash segments).';
2764
+ }
2765
+ if (segments[0] !== "specs") {
2766
+ return "file_path must be rooted at 'specs' (e.g. specs/changes/add-oauth-login/proposal.md).";
2767
+ }
2768
+ if (segments.length < 2) {
2769
+ return "file_path must include a filename under 'specs' (e.g. specs/proposal.md).";
2770
+ }
2771
+ const dirLevels = segments.length - 2;
2772
+ if (dirLevels > MAX_SPEC_DIR_DEPTH) {
2773
+ return `file_path must not nest deeper than ${String(MAX_SPEC_DIR_DEPTH)} directory levels below specs.`;
2774
+ }
2775
+ return null;
2776
+ }
2777
+ var FILE_TYPE_BY_BASENAME = {
2778
+ constitution: "constitution",
2779
+ requirements: "requirements",
2780
+ solution: "solution",
2781
+ tasks: "tasks",
2782
+ proposal: "proposal"
2783
+ };
2784
+ function deriveFileType(filePath) {
2785
+ const filename = filePath.split("/").pop() ?? "";
2786
+ const base = filename.replace(/\.[^.]+$/, "").toLowerCase();
2787
+ return FILE_TYPE_BY_BASENAME[base] ?? "openspec-spec";
2788
+ }
2789
+
2790
+ // src/server/tools/upload_spec_file.ts
2791
+ var SPEC_FILE_TYPES = [
2792
+ "constitution",
2793
+ "requirements",
2794
+ "solution",
2795
+ "tasks",
2796
+ "proposal",
2797
+ "openspec-spec"
2798
+ ];
2799
+ var inputSchema13 = {
2800
+ project_id: z13.string().min(1).describe("UUID of the project to upload the file into."),
2801
+ 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).'
2803
+ ),
2804
+ content: z13.string().min(1).describe("Full UTF-8 text content of the spec file (non-empty)."),
2805
+ 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
+ };
2807
+ var uploadSpecFileTool = {
2808
+ 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).",
2810
+ inputSchema: inputSchema13,
2811
+ handler: async (args, ctx) => {
2812
+ const pathError = validateSpecFilePath(args.file_path);
2813
+ if (pathError) {
2814
+ return errorResult(pathError);
2815
+ }
2816
+ const existing = await ctx.client.findSpecFileByPath(args.project_id, args.file_path);
2817
+ if (existing) {
2818
+ return errorResult(
2819
+ `A spec file already exists at ${args.file_path} in project ${args.project_id}. Use update_spec_file to overwrite it.`
2820
+ );
2821
+ }
2822
+ const fileType = args.file_type ?? deriveFileType(args.file_path);
2823
+ const content = new TextEncoder().encode(args.content);
2824
+ const file = await ctx.client.uploadSpecFile({
2825
+ projectId: args.project_id,
2826
+ filePath: args.file_path,
2827
+ fileType,
2828
+ content
2829
+ });
2830
+ return jsonResult({ file: specFileSummary(file) });
2831
+ }
2832
+ };
2833
+
2471
2834
  // src/server/server.ts
2472
2835
  function buildServer(deps) {
2473
2836
  const server = new McpServer({
@@ -2480,6 +2843,13 @@ function buildServer(deps) {
2480
2843
  registerTool(server, readFileTool, deps.client);
2481
2844
  registerTool(server, getAttachmentTool, deps.client);
2482
2845
  registerTool(server, uploadAttachmentTool, deps.client);
2846
+ registerTool(server, createProjectTool, deps.client);
2847
+ registerTool(server, updateProjectTool, deps.client);
2848
+ registerTool(server, archiveProjectTool, deps.client);
2849
+ registerTool(server, unarchiveProjectTool, deps.client);
2850
+ registerTool(server, deleteProjectTool, deps.client);
2851
+ registerTool(server, uploadSpecFileTool, deps.client);
2852
+ registerTool(server, updateSpecFileTool, deps.client);
2483
2853
  return server;
2484
2854
  }
2485
2855
  async function startStdioServer(deps) {
@@ -2633,7 +3003,7 @@ async function canonicaliseRoot(root) {
2633
3003
  import { promises as fs4 } from "fs";
2634
3004
  import { join as join2, relative } from "path";
2635
3005
  import { spawn } from "child_process";
2636
- import { z as z7 } from "zod";
3006
+ import { z as z14 } from "zod";
2637
3007
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
2638
3008
 
2639
3009
  // src/reverse/ignore.ts
@@ -2712,7 +3082,7 @@ function ripgrepIgnoreGlobs() {
2712
3082
  }
2713
3083
 
2714
3084
  // src/reverse/pack-codebase.ts
2715
- import { createHash as createHash2 } from "crypto";
3085
+ import { createHash as createHash3 } from "crypto";
2716
3086
  import { mkdtemp, readFile as readFile2, rm } from "fs/promises";
2717
3087
  import { tmpdir } from "os";
2718
3088
  import { join } from "path";
@@ -2783,7 +3153,7 @@ function computeOutputId(inputs) {
2783
3153
  i: inputs.includePatterns ?? "",
2784
3154
  x: inputs.ignorePatterns ?? ""
2785
3155
  });
2786
- const digest = createHash2("sha256").update(payload).digest("hex").slice(0, 12);
3156
+ const digest = createHash3("sha256").update(payload).digest("hex").slice(0, 12);
2787
3157
  return `pack-${digest}`;
2788
3158
  }
2789
3159
  function readPage(cache, outputId, page) {
@@ -2883,37 +3253,37 @@ var READ_FILE_MAX_LINES = 2e3;
2883
3253
  var LIST_DIR_HARD_CAP = 5e3;
2884
3254
  var LIST_DIR_DEFAULT_CAP = 1e3;
2885
3255
  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()
3256
+ var listDirInput = z14.object({
3257
+ path: z14.string(),
3258
+ recursive: z14.boolean().optional(),
3259
+ maxEntries: z14.number().int().positive().optional(),
3260
+ includeIgnored: z14.boolean().optional()
2891
3261
  });
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()
3262
+ var readFileInput = z14.object({
3263
+ path: z14.string(),
3264
+ offset: z14.number().int().nonnegative().optional(),
3265
+ limit: z14.number().int().positive().optional(),
3266
+ includeIgnored: z14.boolean().optional()
2897
3267
  });
2898
- var grepInput = z7.object({
2899
- pattern: z7.string().describe(
3268
+ var grepInput = z14.object({
3269
+ pattern: z14.string().describe(
2900
3270
  "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
3271
  ),
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()
3272
+ path: z14.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
3273
+ isRegex: z14.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
3274
+ caseSensitive: z14.boolean().optional(),
3275
+ maxMatches: z14.number().int().positive().optional(),
3276
+ contextLines: z14.number().int().nonnegative().optional(),
3277
+ includeIgnored: z14.boolean().optional()
2908
3278
  });
2909
- var packCodebaseInput = z7.object({
2910
- subpath: z7.string().optional(),
2911
- includePatterns: z7.string().optional(),
2912
- ignorePatterns: z7.string().optional()
3279
+ var packCodebaseInput = z14.object({
3280
+ subpath: z14.string().optional(),
3281
+ includePatterns: z14.string().optional(),
3282
+ ignorePatterns: z14.string().optional()
2913
3283
  });
2914
- var packCodebaseReadPageInput = z7.object({
2915
- outputId: z7.string(),
2916
- page: z7.number().int().positive()
3284
+ var packCodebaseReadPageInput = z14.object({
3285
+ outputId: z14.string(),
3286
+ page: z14.number().int().positive()
2917
3287
  });
2918
3288
  function structuredError(error, message, extra) {
2919
3289
  const payload = { error, message, ...extra ?? {} };
@@ -3441,7 +3811,7 @@ function jsonSchemaFromZod(schema) {
3441
3811
  return zodToJson(schema);
3442
3812
  }
3443
3813
  function zodToJson(schema) {
3444
- if (schema instanceof z7.ZodObject) {
3814
+ if (schema instanceof z14.ZodObject) {
3445
3815
  const shape = schema.shape;
3446
3816
  const properties = {};
3447
3817
  const required = [];
@@ -3456,19 +3826,19 @@ function zodToJson(schema) {
3456
3826
  if (required.length > 0) out.required = required;
3457
3827
  return out;
3458
3828
  }
3459
- if (schema instanceof z7.ZodOptional) {
3829
+ if (schema instanceof z14.ZodOptional) {
3460
3830
  return zodToJson(schema.unwrap());
3461
3831
  }
3462
- if (schema instanceof z7.ZodString) {
3832
+ if (schema instanceof z14.ZodString) {
3463
3833
  return { type: "string" };
3464
3834
  }
3465
- if (schema instanceof z7.ZodNumber) {
3835
+ if (schema instanceof z14.ZodNumber) {
3466
3836
  return { type: "number" };
3467
3837
  }
3468
- if (schema instanceof z7.ZodBoolean) {
3838
+ if (schema instanceof z14.ZodBoolean) {
3469
3839
  return { type: "boolean" };
3470
3840
  }
3471
- if (schema instanceof z7.ZodEnum) {
3841
+ if (schema instanceof z14.ZodEnum) {
3472
3842
  return { type: "string", enum: schema.options };
3473
3843
  }
3474
3844
  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.36",
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": {