@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.
- package/dist/index.js +440 -70
- 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/
|
|
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. 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
|
|
1934
|
-
var
|
|
1935
|
-
file_id:
|
|
1936
|
-
include_download_url:
|
|
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:
|
|
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
|
|
1980
|
-
var
|
|
1981
|
-
project_id:
|
|
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:
|
|
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
|
|
2033
|
-
var
|
|
2034
|
-
query:
|
|
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:
|
|
2038
|
-
offset:
|
|
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:
|
|
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
|
|
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
|
|
2137
|
-
file_id:
|
|
2138
|
-
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:
|
|
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
|
|
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
|
|
2333
|
-
project_id:
|
|
2334
|
-
file_path:
|
|
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:
|
|
2338
|
-
mime_type:
|
|
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:
|
|
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:
|
|
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:${
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
2887
|
-
path:
|
|
2888
|
-
recursive:
|
|
2889
|
-
maxEntries:
|
|
2890
|
-
includeIgnored:
|
|
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 =
|
|
2893
|
-
path:
|
|
2894
|
-
offset:
|
|
2895
|
-
limit:
|
|
2896
|
-
includeIgnored:
|
|
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 =
|
|
2899
|
-
pattern:
|
|
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:
|
|
2903
|
-
isRegex:
|
|
2904
|
-
caseSensitive:
|
|
2905
|
-
maxMatches:
|
|
2906
|
-
contextLines:
|
|
2907
|
-
includeIgnored:
|
|
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 =
|
|
2910
|
-
subpath:
|
|
2911
|
-
includePatterns:
|
|
2912
|
-
ignorePatterns:
|
|
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 =
|
|
2915
|
-
outputId:
|
|
2916
|
-
page:
|
|
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
|
|
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
|
|
3829
|
+
if (schema instanceof z14.ZodOptional) {
|
|
3460
3830
|
return zodToJson(schema.unwrap());
|
|
3461
3831
|
}
|
|
3462
|
-
if (schema instanceof
|
|
3832
|
+
if (schema instanceof z14.ZodString) {
|
|
3463
3833
|
return { type: "string" };
|
|
3464
3834
|
}
|
|
3465
|
-
if (schema instanceof
|
|
3835
|
+
if (schema instanceof z14.ZodNumber) {
|
|
3466
3836
|
return { type: "number" };
|
|
3467
3837
|
}
|
|
3468
|
-
if (schema instanceof
|
|
3838
|
+
if (schema instanceof z14.ZodBoolean) {
|
|
3469
3839
|
return { type: "boolean" };
|
|
3470
3840
|
}
|
|
3471
|
-
if (schema instanceof
|
|
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.
|
|
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": {
|