@myspec/mcp-server 0.1.2-next.33 → 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.
- package/dist/index.js +418 -72
- 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/
|
|
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
|
-
|
|
1899
|
-
|
|
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
|
|
1934
|
-
var
|
|
1935
|
-
file_id:
|
|
1936
|
-
include_download_url:
|
|
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:
|
|
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
|
|
1980
|
-
var
|
|
1981
|
-
project_id:
|
|
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:
|
|
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
|
|
2033
|
-
var
|
|
2034
|
-
query:
|
|
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:
|
|
2038
|
-
offset:
|
|
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:
|
|
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
|
|
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
|
|
2137
|
-
file_id:
|
|
2138
|
-
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:
|
|
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
|
|
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
|
|
2333
|
-
project_id:
|
|
2334
|
-
file_path:
|
|
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:
|
|
2338
|
-
mime_type:
|
|
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:
|
|
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:
|
|
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:${
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
2887
|
-
path:
|
|
2888
|
-
recursive:
|
|
2889
|
-
maxEntries:
|
|
2890
|
-
includeIgnored:
|
|
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 =
|
|
2893
|
-
path:
|
|
2894
|
-
offset:
|
|
2895
|
-
limit:
|
|
2896
|
-
includeIgnored:
|
|
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 =
|
|
2899
|
-
pattern:
|
|
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:
|
|
2903
|
-
isRegex:
|
|
2904
|
-
caseSensitive:
|
|
2905
|
-
maxMatches:
|
|
2906
|
-
contextLines:
|
|
2907
|
-
includeIgnored:
|
|
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 =
|
|
2910
|
-
subpath:
|
|
2911
|
-
includePatterns:
|
|
2912
|
-
ignorePatterns:
|
|
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 =
|
|
2915
|
-
outputId:
|
|
2916
|
-
page:
|
|
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
|
|
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
|
|
3806
|
+
if (schema instanceof z14.ZodOptional) {
|
|
3460
3807
|
return zodToJson(schema.unwrap());
|
|
3461
3808
|
}
|
|
3462
|
-
if (schema instanceof
|
|
3809
|
+
if (schema instanceof z14.ZodString) {
|
|
3463
3810
|
return { type: "string" };
|
|
3464
3811
|
}
|
|
3465
|
-
if (schema instanceof
|
|
3812
|
+
if (schema instanceof z14.ZodNumber) {
|
|
3466
3813
|
return { type: "number" };
|
|
3467
3814
|
}
|
|
3468
|
-
if (schema instanceof
|
|
3815
|
+
if (schema instanceof z14.ZodBoolean) {
|
|
3469
3816
|
return { type: "boolean" };
|
|
3470
3817
|
}
|
|
3471
|
-
if (schema instanceof
|
|
3818
|
+
if (schema instanceof z14.ZodEnum) {
|
|
3472
3819
|
return { type: "string", enum: schema.options };
|
|
3473
3820
|
}
|
|
3474
3821
|
return { type: "string" };
|
|
@@ -3487,8 +3834,7 @@ async function runReverse(opts) {
|
|
|
3487
3834
|
const realRoot = await canonicaliseRoot(opts.root);
|
|
3488
3835
|
const rootLabel = basename3(realRoot);
|
|
3489
3836
|
process.stderr.write(
|
|
3490
|
-
`myspec-mcp reverse: granting read access to ${realRoot}
|
|
3491
|
-
Press Ctrl-C to stop. Auto-reconnect enabled.
|
|
3837
|
+
`myspec-mcp reverse: granting read access to ${realRoot}Press Ctrl-C to stop. Auto-reconnect enabled.
|
|
3492
3838
|
`
|
|
3493
3839
|
);
|
|
3494
3840
|
let backoffMs = RECONNECT_BACKOFF_INITIAL_MS;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myspec/mcp-server",
|
|
3
|
-
"version": "0.1.2-next.
|
|
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": {
|