@myspec/mcp-server 0.1.4-next.52 → 0.1.4-next.54
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/README.md +2 -1
- package/dist/index.js +275 -192
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,7 +90,8 @@ Local state is stored under `~/.myspec/` (alongside the on-disk cache root), spl
|
|
|
90
90
|
| `list_projects` | `query?`, `limit?`, `offset?` | Paginated list of projects |
|
|
91
91
|
| `get_project` | `project_id` | Project + active sessions + files + attachments |
|
|
92
92
|
| `get_spec_file` | `file_id`, optional `include_download_url` | File metadata; with `include_download_url=N` also returns a download URL targeting revision `N` |
|
|
93
|
-
| `read_spec_file` | `file_id`, optional `revision` | UTF-8 content of the file (errors for binary). Cached on disk under `MYSPEC_DOWNLOAD_ROOT` |
|
|
93
|
+
| `read_spec_file` | `file_id`, optional `revision` | UTF-8 content of the file (errors for binary or files > 1 MiB — use `download_spec_file` for those). Cached on disk under `MYSPEC_DOWNLOAD_ROOT` |
|
|
94
|
+
| `download_spec_file` | `file_id`, `destination_path`, optional `revision`, optional `overwrite` | Downloads the file's raw bytes to `destination_path` (absolute or cwd-relative; an existing directory gets the basename appended). Handles binary files and files up to 50 MiB. Refuses to replace an existing file unless `overwrite=true`. Returns only metadata (`saved_path`, `bytes_written`, `revision_number`) — **not** the content, so a large file can be saved locally and read in chunks |
|
|
94
95
|
| `list_spec_file` | `project_id`, optional `path`, optional `trashed` | Lists a project's spec files (`file_id`, `file_path`, `file_type`, `file_size_bytes`, `revision_count`, `session_id`, `updated_at`). Filter to a directory with `path` (exact file or everything under it; `""`/`"/"`/omit = all). `trashed=true` lists the Trash Bin instead of live files |
|
|
95
96
|
| `move_spec_file_to_trash` | `file_id` | Moves a spec file to the Trash Bin (recoverable — hidden from listings and AI-agent tools, restorable indefinitely). Not a permanent delete |
|
|
96
97
|
| `restore_spec_file_from_trash` | `file_id` | Restores a trashed spec file back to the active file list, reversing `move_spec_file_to_trash` |
|
package/dist/index.js
CHANGED
|
@@ -2132,16 +2132,177 @@ var deleteProjectTool = {
|
|
|
2132
2132
|
}
|
|
2133
2133
|
};
|
|
2134
2134
|
|
|
2135
|
-
// src/server/tools/
|
|
2135
|
+
// src/server/tools/download_spec_file.ts
|
|
2136
|
+
import path3 from "path";
|
|
2136
2137
|
import { z as z4 } from "zod";
|
|
2138
|
+
|
|
2139
|
+
// src/server/download-path.ts
|
|
2140
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
2141
|
+
import { promises as fs2 } from "fs";
|
|
2142
|
+
import os2 from "os";
|
|
2143
|
+
import path2 from "path";
|
|
2144
|
+
function resolveDownloadRoot(env = process.env) {
|
|
2145
|
+
const configured = env.MYSPEC_DOWNLOAD_ROOT;
|
|
2146
|
+
if (configured) {
|
|
2147
|
+
if (!path2.isAbsolute(configured)) {
|
|
2148
|
+
throw new Error(`MYSPEC_DOWNLOAD_ROOT must be an absolute path (got: ${configured})`);
|
|
2149
|
+
}
|
|
2150
|
+
return path2.resolve(configured);
|
|
2151
|
+
}
|
|
2152
|
+
return path2.join(os2.homedir(), ".myspec");
|
|
2153
|
+
}
|
|
2154
|
+
function assertWithinRoot(target, root) {
|
|
2155
|
+
const resolvedTarget = path2.resolve(target);
|
|
2156
|
+
const resolvedRoot = path2.resolve(root);
|
|
2157
|
+
const rel = path2.relative(resolvedRoot, resolvedTarget);
|
|
2158
|
+
if (rel.startsWith("..") || path2.isAbsolute(rel)) {
|
|
2159
|
+
return `path resolves outside the configured root (${resolvedRoot}): ${resolvedTarget}`;
|
|
2160
|
+
}
|
|
2161
|
+
return null;
|
|
2162
|
+
}
|
|
2163
|
+
function computeFileCachePath(args) {
|
|
2164
|
+
const basename4 = path2.basename(args.filePath) || "content";
|
|
2165
|
+
return path2.join(
|
|
2166
|
+
args.cacheRoot,
|
|
2167
|
+
"project",
|
|
2168
|
+
args.projectId,
|
|
2169
|
+
"file",
|
|
2170
|
+
args.fileId,
|
|
2171
|
+
"rev",
|
|
2172
|
+
String(args.revision),
|
|
2173
|
+
basename4
|
|
2174
|
+
);
|
|
2175
|
+
}
|
|
2176
|
+
function maskHomedir(p, home = os2.homedir()) {
|
|
2177
|
+
if (!home) {
|
|
2178
|
+
return p;
|
|
2179
|
+
}
|
|
2180
|
+
if (p === home) {
|
|
2181
|
+
return "~";
|
|
2182
|
+
}
|
|
2183
|
+
if (p.startsWith(home + path2.sep)) {
|
|
2184
|
+
return "~" + p.slice(home.length);
|
|
2185
|
+
}
|
|
2186
|
+
return p;
|
|
2187
|
+
}
|
|
2188
|
+
async function resolveDestinationFile(destPath, basename4) {
|
|
2189
|
+
const absPath = path2.isAbsolute(destPath) ? path2.resolve(destPath) : path2.resolve(process.cwd(), destPath);
|
|
2190
|
+
const endsWithSep = destPath.endsWith(path2.sep) || destPath.endsWith("/");
|
|
2191
|
+
let isDir = endsWithSep;
|
|
2192
|
+
if (!isDir) {
|
|
2193
|
+
try {
|
|
2194
|
+
const info = await fs2.stat(absPath);
|
|
2195
|
+
isDir = info.isDirectory();
|
|
2196
|
+
} catch (err) {
|
|
2197
|
+
if (!isFsNotFound2(err)) {
|
|
2198
|
+
throw err;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
return isDir ? path2.join(absPath, basename4) : absPath;
|
|
2203
|
+
}
|
|
2204
|
+
async function pathExists(p) {
|
|
2205
|
+
try {
|
|
2206
|
+
await fs2.stat(p);
|
|
2207
|
+
return true;
|
|
2208
|
+
} catch (err) {
|
|
2209
|
+
if (isFsNotFound2(err)) {
|
|
2210
|
+
return false;
|
|
2211
|
+
}
|
|
2212
|
+
throw err;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
function isFsNotFound2(err) {
|
|
2216
|
+
const code = typeof err === "object" && err !== null ? err.code : void 0;
|
|
2217
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
2218
|
+
}
|
|
2219
|
+
async function writeBytesAtomically(target, bytes) {
|
|
2220
|
+
const dir = path2.dirname(target);
|
|
2221
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
2222
|
+
const suffix = randomBytes2(6).toString("hex");
|
|
2223
|
+
const tmp = path2.join(dir, `.${path2.basename(target)}.${process.pid.toString()}.${suffix}.tmp`);
|
|
2224
|
+
const handle = await fs2.open(tmp, "wx", 384);
|
|
2225
|
+
try {
|
|
2226
|
+
try {
|
|
2227
|
+
await handle.write(bytes);
|
|
2228
|
+
} finally {
|
|
2229
|
+
await handle.close();
|
|
2230
|
+
}
|
|
2231
|
+
await fs2.rename(tmp, target);
|
|
2232
|
+
} catch (err) {
|
|
2233
|
+
await fs2.rm(tmp, { force: true });
|
|
2234
|
+
throw err;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
// src/server/tools/download_spec_file.ts
|
|
2239
|
+
var MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024;
|
|
2137
2240
|
var inputSchema4 = {
|
|
2138
|
-
|
|
2139
|
-
|
|
2241
|
+
file_id: z4.string().min(1).describe("UUID of the file"),
|
|
2242
|
+
revision: z4.number().int().positive().optional().describe("Specific revision number to download (defaults to the latest revision)."),
|
|
2243
|
+
destination_path: z4.string().min(1).describe(
|
|
2244
|
+
"Local path to save the file to. Absolute, or relative to the MCP server's working directory. If it points to an existing directory (or ends with a path separator), the spec file's basename is appended."
|
|
2245
|
+
),
|
|
2246
|
+
overwrite: z4.boolean().optional().describe("Overwrite the destination file if it already exists (default: false).")
|
|
2247
|
+
};
|
|
2248
|
+
var downloadSpecFileTool = {
|
|
2249
|
+
name: "download_spec_file",
|
|
2250
|
+
description: "Download a spec file's raw bytes to a local file at `destination_path`. Unlike read_spec_file, this handles binary files and files up to 50 MiB, and the response returns only metadata (saved path, size, revision) \u2014 not the content. Use it to save a large or binary file locally, then search or read it in chunks with other tools.",
|
|
2251
|
+
inputSchema: inputSchema4,
|
|
2252
|
+
handler: async (args, ctx) => {
|
|
2253
|
+
const meta = await ctx.client.getFileMetadata(args.file_id);
|
|
2254
|
+
const targetRevision = args.revision ?? meta.revisionCount;
|
|
2255
|
+
if (targetRevision === meta.revisionCount && meta.fileSizeBytes > MAX_DOWNLOAD_BYTES) {
|
|
2256
|
+
return errorResult(buildOversizeMessage(args.file_id, meta.fileSizeBytes));
|
|
2257
|
+
}
|
|
2258
|
+
const basename4 = path3.basename(meta.filePath) || "content";
|
|
2259
|
+
let destFile;
|
|
2260
|
+
try {
|
|
2261
|
+
destFile = await resolveDestinationFile(args.destination_path, basename4);
|
|
2262
|
+
} catch (err) {
|
|
2263
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2264
|
+
return errorResult(`Failed to resolve destination_path ${args.destination_path}: ${message}`);
|
|
2265
|
+
}
|
|
2266
|
+
if (!args.overwrite && await pathExists(destFile)) {
|
|
2267
|
+
return errorResult(
|
|
2268
|
+
`Destination already exists: ${maskHomedir(destFile)}. Pass overwrite: true to replace it.`
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
const downloaded = await ctx.client.downloadFile(args.file_id, args.revision);
|
|
2272
|
+
const bytes = downloaded.bytes;
|
|
2273
|
+
if (bytes.byteLength > MAX_DOWNLOAD_BYTES) {
|
|
2274
|
+
return errorResult(buildOversizeMessage(args.file_id, bytes.byteLength));
|
|
2275
|
+
}
|
|
2276
|
+
try {
|
|
2277
|
+
await writeBytesAtomically(destFile, bytes);
|
|
2278
|
+
} catch (err) {
|
|
2279
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2280
|
+
return errorResult(`Failed to write ${maskHomedir(destFile)}: ${message}`);
|
|
2281
|
+
}
|
|
2282
|
+
return jsonResult({
|
|
2283
|
+
file_id: meta.id,
|
|
2284
|
+
revision_number: downloaded.revisionNumber,
|
|
2285
|
+
file_path: meta.filePath,
|
|
2286
|
+
file_type: meta.fileType,
|
|
2287
|
+
saved_path: maskHomedir(destFile),
|
|
2288
|
+
bytes_written: bytes.byteLength
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
};
|
|
2292
|
+
function buildOversizeMessage(fileId, byteLength) {
|
|
2293
|
+
return `File ${fileId} is ${String(byteLength)} bytes which exceeds the download cap of ${String(MAX_DOWNLOAD_BYTES)} bytes.`;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
// src/server/tools/get_attachment.ts
|
|
2297
|
+
import { z as z5 } from "zod";
|
|
2298
|
+
var inputSchema5 = {
|
|
2299
|
+
attachment_id: z5.string().min(1).describe("UUID of the attachment"),
|
|
2300
|
+
include_download_url: z5.number().int().nonnegative().optional().describe("Set to any positive value to include a signed download URL. Omit or 0 to skip.")
|
|
2140
2301
|
};
|
|
2141
2302
|
var getAttachmentTool = {
|
|
2142
2303
|
name: "get_attachment",
|
|
2143
2304
|
description: "Get attachment metadata. With `include_download_url>0`, also returns a signed `download_url`.",
|
|
2144
|
-
inputSchema:
|
|
2305
|
+
inputSchema: inputSchema5,
|
|
2145
2306
|
handler: async (args, ctx) => {
|
|
2146
2307
|
const attachment = await ctx.client.getAttachmentMetadata(args.attachment_id);
|
|
2147
2308
|
if (!attachment) {
|
|
@@ -2170,14 +2331,14 @@ var getAttachmentTool = {
|
|
|
2170
2331
|
};
|
|
2171
2332
|
|
|
2172
2333
|
// src/server/tools/get_project.ts
|
|
2173
|
-
import { z as
|
|
2174
|
-
var
|
|
2175
|
-
project_id:
|
|
2334
|
+
import { z as z6 } from "zod";
|
|
2335
|
+
var inputSchema6 = {
|
|
2336
|
+
project_id: z6.string().min(1).describe("UUID of the project to fetch")
|
|
2176
2337
|
};
|
|
2177
2338
|
var getProjectTool = {
|
|
2178
2339
|
name: "get_project",
|
|
2179
2340
|
description: "Get details for a project including all active spec sessions, files (id + metadata), and attachments (id + metadata).",
|
|
2180
|
-
inputSchema:
|
|
2341
|
+
inputSchema: inputSchema6,
|
|
2181
2342
|
handler: async (args, ctx) => {
|
|
2182
2343
|
const [project, sessions, files, attachments] = await Promise.all([
|
|
2183
2344
|
ctx.client.getProject(args.project_id),
|
|
@@ -2223,17 +2384,17 @@ var getProjectTool = {
|
|
|
2223
2384
|
};
|
|
2224
2385
|
|
|
2225
2386
|
// src/server/tools/get_spec_file.ts
|
|
2226
|
-
import { z as
|
|
2227
|
-
var
|
|
2228
|
-
file_id:
|
|
2229
|
-
include_download_url:
|
|
2387
|
+
import { z as z7 } from "zod";
|
|
2388
|
+
var inputSchema7 = {
|
|
2389
|
+
file_id: z7.string().min(1).describe("UUID of the file"),
|
|
2390
|
+
include_download_url: z7.number().int().nonnegative().optional().describe(
|
|
2230
2391
|
"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."
|
|
2231
2392
|
)
|
|
2232
2393
|
};
|
|
2233
2394
|
var getSpecFileTool = {
|
|
2234
2395
|
name: "get_spec_file",
|
|
2235
2396
|
description: "Get spec 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.",
|
|
2236
|
-
inputSchema:
|
|
2397
|
+
inputSchema: inputSchema7,
|
|
2237
2398
|
handler: async (args, ctx) => {
|
|
2238
2399
|
const meta = await ctx.client.getFileMetadata(args.file_id);
|
|
2239
2400
|
const base = {
|
|
@@ -2269,18 +2430,18 @@ var getSpecFileTool = {
|
|
|
2269
2430
|
};
|
|
2270
2431
|
|
|
2271
2432
|
// src/server/tools/list_projects.ts
|
|
2272
|
-
import { z as
|
|
2273
|
-
var
|
|
2274
|
-
query:
|
|
2433
|
+
import { z as z8 } from "zod";
|
|
2434
|
+
var inputSchema8 = {
|
|
2435
|
+
query: z8.string().optional().describe(
|
|
2275
2436
|
"Search term to filter projects by name. Omit to list all projects."
|
|
2276
2437
|
),
|
|
2277
|
-
limit:
|
|
2278
|
-
offset:
|
|
2438
|
+
limit: z8.number().int().positive().max(100).optional().describe("Max projects to return (default 20)"),
|
|
2439
|
+
offset: z8.number().int().nonnegative().optional().describe("Pagination offset (default 0)")
|
|
2279
2440
|
};
|
|
2280
2441
|
var listProjectsTool = {
|
|
2281
2442
|
name: "list_projects",
|
|
2282
2443
|
description: "List or search MySpec projects accessible to the authenticated user.",
|
|
2283
|
-
inputSchema:
|
|
2444
|
+
inputSchema: inputSchema8,
|
|
2284
2445
|
handler: async (args, ctx) => {
|
|
2285
2446
|
const result = await ctx.client.listProjects({
|
|
2286
2447
|
query: args.query,
|
|
@@ -2303,21 +2464,21 @@ var listProjectsTool = {
|
|
|
2303
2464
|
};
|
|
2304
2465
|
|
|
2305
2466
|
// src/server/tools/list_spec_file.ts
|
|
2306
|
-
import { z as
|
|
2467
|
+
import { z as z9 } from "zod";
|
|
2307
2468
|
var PAGE_SIZE = 100;
|
|
2308
|
-
var
|
|
2309
|
-
project_id:
|
|
2310
|
-
path:
|
|
2469
|
+
var inputSchema9 = {
|
|
2470
|
+
project_id: z9.string().min(1).describe("UUID of the project whose spec files to list."),
|
|
2471
|
+
path: z9.string().optional().describe(
|
|
2311
2472
|
'Directory prefix to filter by, e.g. ".spec" or ".spec/design". Matches the exact file and everything under that directory. Pass "", "/", or omit to list all files.'
|
|
2312
2473
|
),
|
|
2313
|
-
trashed:
|
|
2474
|
+
trashed: z9.boolean().optional().describe(
|
|
2314
2475
|
"When true, list files in the project Trash Bin instead. When false or omitted, list only live files (trashed files excluded)."
|
|
2315
2476
|
)
|
|
2316
2477
|
};
|
|
2317
2478
|
var listSpecFileTool = {
|
|
2318
2479
|
name: "list_spec_file",
|
|
2319
2480
|
description: 'List a project\'s spec files with id, path, and metadata (revision count, file size of the latest revision, file type, owning session, last update). Filter to a directory with `path` (exact file or everything under it; pass "", "/", or omit for all). Set `trashed=true` to list files in the Trash Bin instead of live files.',
|
|
2320
|
-
inputSchema:
|
|
2481
|
+
inputSchema: inputSchema9,
|
|
2321
2482
|
handler: async (args, ctx) => {
|
|
2322
2483
|
const trashed = args.trashed ?? false;
|
|
2323
2484
|
const files = trashed ? await ctx.client.listTrashedFiles(args.project_id) : await listAllLiveFiles(ctx.client, args.project_id);
|
|
@@ -2374,14 +2535,14 @@ function isUnderPath(filePath, prefix) {
|
|
|
2374
2535
|
}
|
|
2375
2536
|
|
|
2376
2537
|
// src/server/tools/move_spec_file_to_trash.ts
|
|
2377
|
-
import { z as
|
|
2378
|
-
var
|
|
2379
|
-
file_id:
|
|
2538
|
+
import { z as z10 } from "zod";
|
|
2539
|
+
var inputSchema10 = {
|
|
2540
|
+
file_id: z10.string().min(1).describe("UUID of the spec file to move to the Trash Bin.")
|
|
2380
2541
|
};
|
|
2381
2542
|
var moveSpecFileToTrashTool = {
|
|
2382
2543
|
name: "move_spec_file_to_trash",
|
|
2383
2544
|
description: "Move a spec file to the Trash Bin. This is recoverable \u2014 the file is hidden from the default file list and AI-agent tools but kept restorable indefinitely via `restore_spec_file_from_trash`. It is NOT a permanent delete.",
|
|
2384
|
-
inputSchema:
|
|
2545
|
+
inputSchema: inputSchema10,
|
|
2385
2546
|
handler: async (args, ctx) => {
|
|
2386
2547
|
await ctx.client.trashFile(args.file_id);
|
|
2387
2548
|
return jsonResult({ trashed: true, file_id: args.file_id });
|
|
@@ -2390,88 +2551,21 @@ var moveSpecFileToTrashTool = {
|
|
|
2390
2551
|
|
|
2391
2552
|
// src/server/tools/read_spec_file.ts
|
|
2392
2553
|
import { promises as fs3 } from "fs";
|
|
2393
|
-
import
|
|
2394
|
-
import path3 from "path";
|
|
2395
|
-
import { z as z10 } from "zod";
|
|
2396
|
-
|
|
2397
|
-
// src/server/download-path.ts
|
|
2398
|
-
import { promises as fs2 } from "fs";
|
|
2399
|
-
import os2 from "os";
|
|
2400
|
-
import path2 from "path";
|
|
2401
|
-
function resolveDownloadRoot(env = process.env) {
|
|
2402
|
-
const configured = env.MYSPEC_DOWNLOAD_ROOT;
|
|
2403
|
-
if (configured) {
|
|
2404
|
-
if (!path2.isAbsolute(configured)) {
|
|
2405
|
-
throw new Error(`MYSPEC_DOWNLOAD_ROOT must be an absolute path (got: ${configured})`);
|
|
2406
|
-
}
|
|
2407
|
-
return path2.resolve(configured);
|
|
2408
|
-
}
|
|
2409
|
-
return path2.join(os2.homedir(), ".myspec");
|
|
2410
|
-
}
|
|
2411
|
-
function assertWithinRoot(target, root) {
|
|
2412
|
-
const resolvedTarget = path2.resolve(target);
|
|
2413
|
-
const resolvedRoot = path2.resolve(root);
|
|
2414
|
-
const rel = path2.relative(resolvedRoot, resolvedTarget);
|
|
2415
|
-
if (rel.startsWith("..") || path2.isAbsolute(rel)) {
|
|
2416
|
-
return `path resolves outside the configured root (${resolvedRoot}): ${resolvedTarget}`;
|
|
2417
|
-
}
|
|
2418
|
-
return null;
|
|
2419
|
-
}
|
|
2420
|
-
function computeFileCachePath(args) {
|
|
2421
|
-
const basename4 = path2.basename(args.filePath) || "content";
|
|
2422
|
-
return path2.join(
|
|
2423
|
-
args.cacheRoot,
|
|
2424
|
-
"project",
|
|
2425
|
-
args.projectId,
|
|
2426
|
-
"file",
|
|
2427
|
-
args.fileId,
|
|
2428
|
-
"rev",
|
|
2429
|
-
String(args.revision),
|
|
2430
|
-
basename4
|
|
2431
|
-
);
|
|
2432
|
-
}
|
|
2433
|
-
async function pathExists(p) {
|
|
2434
|
-
try {
|
|
2435
|
-
await fs2.stat(p);
|
|
2436
|
-
return true;
|
|
2437
|
-
} catch (err) {
|
|
2438
|
-
if (isFsNotFound2(err)) {
|
|
2439
|
-
return false;
|
|
2440
|
-
}
|
|
2441
|
-
throw err;
|
|
2442
|
-
}
|
|
2443
|
-
}
|
|
2444
|
-
function isFsNotFound2(err) {
|
|
2445
|
-
return typeof err === "object" && err !== null && err.code === "ENOENT";
|
|
2446
|
-
}
|
|
2447
|
-
async function writeBytesAtomically(target, bytes) {
|
|
2448
|
-
const dir = path2.dirname(target);
|
|
2449
|
-
await fs2.mkdir(dir, { recursive: true });
|
|
2450
|
-
const tmp = path2.join(dir, `.${path2.basename(target)}.${process.pid.toString()}.tmp`);
|
|
2451
|
-
const handle = await fs2.open(tmp, "wx", 384);
|
|
2452
|
-
try {
|
|
2453
|
-
await handle.write(bytes);
|
|
2454
|
-
} finally {
|
|
2455
|
-
await handle.close();
|
|
2456
|
-
}
|
|
2457
|
-
await fs2.rename(tmp, target);
|
|
2458
|
-
}
|
|
2459
|
-
|
|
2460
|
-
// src/server/tools/read_spec_file.ts
|
|
2554
|
+
import { z as z11 } from "zod";
|
|
2461
2555
|
var MAX_READ_BYTES = 1 * 1024 * 1024;
|
|
2462
|
-
var
|
|
2463
|
-
file_id:
|
|
2464
|
-
revision:
|
|
2556
|
+
var inputSchema11 = {
|
|
2557
|
+
file_id: z11.string().min(1).describe("UUID of the file"),
|
|
2558
|
+
revision: z11.number().int().positive().optional().describe("Specific revision number to read (defaults to the latest revision).")
|
|
2465
2559
|
};
|
|
2466
2560
|
var readSpecFileTool = {
|
|
2467
2561
|
name: "read_spec_file",
|
|
2468
|
-
description: "Return a spec 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
|
|
2469
|
-
inputSchema:
|
|
2562
|
+
description: "Return a spec 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 download_spec_file with a `destination_path` to save those to a local file instead.",
|
|
2563
|
+
inputSchema: inputSchema11,
|
|
2470
2564
|
handler: async (args, ctx) => {
|
|
2471
2565
|
const meta = await ctx.client.getFileMetadata(args.file_id);
|
|
2472
2566
|
const targetRevision = args.revision ?? meta.revisionCount;
|
|
2473
2567
|
if (targetRevision === meta.revisionCount && meta.fileSizeBytes > MAX_READ_BYTES) {
|
|
2474
|
-
return errorResult(
|
|
2568
|
+
return errorResult(buildOversizeMessage2(args.file_id, meta.fileSizeBytes));
|
|
2475
2569
|
}
|
|
2476
2570
|
const cacheRoot = resolveDownloadRoot();
|
|
2477
2571
|
const cachePath = computeFileCachePath({
|
|
@@ -2496,16 +2590,16 @@ var readSpecFileTool = {
|
|
|
2496
2590
|
const downloaded = await ctx.client.downloadFile(args.file_id, args.revision);
|
|
2497
2591
|
bytes = downloaded.bytes;
|
|
2498
2592
|
if (bytes.byteLength > MAX_READ_BYTES) {
|
|
2499
|
-
return errorResult(
|
|
2593
|
+
return errorResult(buildOversizeMessage2(args.file_id, bytes.byteLength));
|
|
2500
2594
|
}
|
|
2501
2595
|
await writeBytesAtomically(cachePath, bytes);
|
|
2502
2596
|
}
|
|
2503
2597
|
if (bytes.byteLength > MAX_READ_BYTES) {
|
|
2504
|
-
return errorResult(
|
|
2598
|
+
return errorResult(buildOversizeMessage2(args.file_id, bytes.byteLength));
|
|
2505
2599
|
}
|
|
2506
2600
|
if (isBinary(bytes)) {
|
|
2507
2601
|
return errorResult(
|
|
2508
|
-
`File ${args.file_id} appears to be binary (file_type=${meta.fileType}, size=${String(bytes.byteLength)} bytes). Use
|
|
2602
|
+
`File ${args.file_id} appears to be binary (file_type=${meta.fileType}, size=${String(bytes.byteLength)} bytes). Use download_spec_file with a \`destination_path\` to save it to a local file.`
|
|
2509
2603
|
);
|
|
2510
2604
|
}
|
|
2511
2605
|
let text;
|
|
@@ -2513,7 +2607,7 @@ var readSpecFileTool = {
|
|
|
2513
2607
|
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
2514
2608
|
} catch {
|
|
2515
2609
|
return errorResult(
|
|
2516
|
-
`File ${args.file_id} is not valid UTF-8 text. Use
|
|
2610
|
+
`File ${args.file_id} is not valid UTF-8 text. Use download_spec_file with a \`destination_path\` to save it to a local file.`
|
|
2517
2611
|
);
|
|
2518
2612
|
}
|
|
2519
2613
|
return jsonResult({
|
|
@@ -2527,20 +2621,8 @@ var readSpecFileTool = {
|
|
|
2527
2621
|
});
|
|
2528
2622
|
}
|
|
2529
2623
|
};
|
|
2530
|
-
function
|
|
2531
|
-
return `File ${fileId} is ${String(byteLength)} bytes which exceeds the read cap of ${String(MAX_READ_BYTES)} bytes. Use
|
|
2532
|
-
}
|
|
2533
|
-
function maskHomedir(p, home = os3.homedir()) {
|
|
2534
|
-
if (!home) {
|
|
2535
|
-
return p;
|
|
2536
|
-
}
|
|
2537
|
-
if (p === home) {
|
|
2538
|
-
return "~";
|
|
2539
|
-
}
|
|
2540
|
-
if (p.startsWith(home + path3.sep)) {
|
|
2541
|
-
return "~" + p.slice(home.length);
|
|
2542
|
-
}
|
|
2543
|
-
return p;
|
|
2624
|
+
function buildOversizeMessage2(fileId, byteLength) {
|
|
2625
|
+
return `File ${fileId} is ${String(byteLength)} bytes which exceeds the read cap of ${String(MAX_READ_BYTES)} bytes. Use download_spec_file with a \`destination_path\` to save it to a local file and read it in chunks.`;
|
|
2544
2626
|
}
|
|
2545
2627
|
function isBinary(bytes) {
|
|
2546
2628
|
for (const byte of bytes) {
|
|
@@ -2552,14 +2634,14 @@ function isBinary(bytes) {
|
|
|
2552
2634
|
}
|
|
2553
2635
|
|
|
2554
2636
|
// src/server/tools/restore_spec_file_from_trash.ts
|
|
2555
|
-
import { z as
|
|
2556
|
-
var
|
|
2557
|
-
file_id:
|
|
2637
|
+
import { z as z12 } from "zod";
|
|
2638
|
+
var inputSchema12 = {
|
|
2639
|
+
file_id: z12.string().min(1).describe("UUID of the spec file to restore from the Trash Bin.")
|
|
2558
2640
|
};
|
|
2559
2641
|
var restoreSpecFileFromTrashTool = {
|
|
2560
2642
|
name: "restore_spec_file_from_trash",
|
|
2561
2643
|
description: "Restore a spec file from the Trash Bin back to the active file list, reversing `move_spec_file_to_trash`. The file becomes visible again to the default file list and AI-agent tools.",
|
|
2562
|
-
inputSchema:
|
|
2644
|
+
inputSchema: inputSchema12,
|
|
2563
2645
|
handler: async (args, ctx) => {
|
|
2564
2646
|
await ctx.client.restoreFile(args.file_id);
|
|
2565
2647
|
return jsonResult({ restored: true, file_id: args.file_id });
|
|
@@ -2567,14 +2649,14 @@ var restoreSpecFileFromTrashTool = {
|
|
|
2567
2649
|
};
|
|
2568
2650
|
|
|
2569
2651
|
// src/server/tools/unarchive_project.ts
|
|
2570
|
-
import { z as
|
|
2571
|
-
var
|
|
2572
|
-
project_id:
|
|
2652
|
+
import { z as z13 } from "zod";
|
|
2653
|
+
var inputSchema13 = {
|
|
2654
|
+
project_id: z13.string().min(1).describe("UUID of the project to unarchive.")
|
|
2573
2655
|
};
|
|
2574
2656
|
var unarchiveProjectTool = {
|
|
2575
2657
|
name: "unarchive_project",
|
|
2576
2658
|
description: 'Unarchive a project (sets status to "active"). Idempotent: unarchiving an already-active project succeeds.',
|
|
2577
|
-
inputSchema:
|
|
2659
|
+
inputSchema: inputSchema13,
|
|
2578
2660
|
handler: async (args, ctx) => {
|
|
2579
2661
|
const project = await ctx.client.unarchiveProject(args.project_id);
|
|
2580
2662
|
return jsonResult({ project: projectSummary(project) });
|
|
@@ -2582,19 +2664,19 @@ var unarchiveProjectTool = {
|
|
|
2582
2664
|
};
|
|
2583
2665
|
|
|
2584
2666
|
// src/server/tools/update_project.ts
|
|
2585
|
-
import { z as
|
|
2586
|
-
var
|
|
2587
|
-
project_id:
|
|
2588
|
-
name:
|
|
2589
|
-
description:
|
|
2590
|
-
project_type:
|
|
2667
|
+
import { z as z14 } from "zod";
|
|
2668
|
+
var inputSchema14 = {
|
|
2669
|
+
project_id: z14.string().min(1).describe("UUID of the project to update."),
|
|
2670
|
+
name: z14.string().min(1).optional().describe("New project name."),
|
|
2671
|
+
description: z14.string().optional().describe("New project description."),
|
|
2672
|
+
project_type: z14.enum(["greenfield", "brownfield", "auto"]).optional().describe(
|
|
2591
2673
|
"Codebase type. 'greenfield' or 'brownfield' sets it explicitly; 'auto' clears it so the workflow decides. Omit to leave unchanged."
|
|
2592
2674
|
)
|
|
2593
2675
|
};
|
|
2594
2676
|
var updateProjectTool = {
|
|
2595
2677
|
name: "update_project",
|
|
2596
2678
|
description: "Update a project's metadata (name, description, project_type). Returns the updated metadata.",
|
|
2597
|
-
inputSchema:
|
|
2679
|
+
inputSchema: inputSchema14,
|
|
2598
2680
|
handler: async (args, ctx) => {
|
|
2599
2681
|
if (args.name === void 0 && args.description === void 0 && args.project_type === void 0) {
|
|
2600
2682
|
return errorResult(
|
|
@@ -2617,7 +2699,7 @@ var updateProjectTool = {
|
|
|
2617
2699
|
};
|
|
2618
2700
|
|
|
2619
2701
|
// src/server/tools/update_spec_file.ts
|
|
2620
|
-
import { z as
|
|
2702
|
+
import { z as z15 } from "zod";
|
|
2621
2703
|
|
|
2622
2704
|
// src/server/tools/spec-content-source.ts
|
|
2623
2705
|
import { readFile, stat } from "fs/promises";
|
|
@@ -2689,21 +2771,21 @@ function specFileSummary(file) {
|
|
|
2689
2771
|
}
|
|
2690
2772
|
|
|
2691
2773
|
// src/server/tools/update_spec_file.ts
|
|
2692
|
-
var
|
|
2693
|
-
file_id:
|
|
2694
|
-
project_id:
|
|
2695
|
-
file_path:
|
|
2696
|
-
content:
|
|
2774
|
+
var inputSchema15 = {
|
|
2775
|
+
file_id: z15.string().min(1).optional().describe("UUID of the spec file to overwrite. Provide this or (project_id + file_path)."),
|
|
2776
|
+
project_id: z15.string().min(1).optional().describe("UUID of the project. Required (with file_path) when file_id is not given."),
|
|
2777
|
+
file_path: z15.string().min(1).optional().describe("Path of the spec file. Required (with project_id) when file_id is not given."),
|
|
2778
|
+
content: z15.string().min(1).optional().describe(
|
|
2697
2779
|
"Full UTF-8 replacement content, saved as a new revision. Provide this OR local_file_path (exactly one). Use for in-memory / generated content."
|
|
2698
2780
|
),
|
|
2699
|
-
local_file_path:
|
|
2781
|
+
local_file_path: z15.string().min(1).optional().describe(
|
|
2700
2782
|
"Path to a local file whose bytes become the new revision; the server reads it directly. Provide this OR content (exactly one). Absolute paths are most reliable; a relative path resolves against the MCP server working directory. Point this only at an intended spec file \u2014 its bytes are uploaded to the project as-is; never use it for secrets or unrelated files."
|
|
2701
2783
|
)
|
|
2702
2784
|
};
|
|
2703
2785
|
var updateSpecFileTool = {
|
|
2704
2786
|
name: "update_spec_file",
|
|
2705
2787
|
description: "Overwrite an existing spec file by saving a new revision. Provide the new body inline via `content` or by pointing at a local file with `local_file_path` \u2014 exactly one is required. Prefer `local_file_path` when the body is already an existing local file (the server reads it directly); use `content` for text generated in-memory. Reference the file by file_id or by (project_id + file_path).",
|
|
2706
|
-
inputSchema:
|
|
2788
|
+
inputSchema: inputSchema15,
|
|
2707
2789
|
handler: async (args, ctx) => {
|
|
2708
2790
|
const resolved = await resolveSpecContent({
|
|
2709
2791
|
content: args.content,
|
|
@@ -2739,7 +2821,7 @@ var updateSpecFileTool = {
|
|
|
2739
2821
|
import { createHash as createHash2 } from "crypto";
|
|
2740
2822
|
import { lstat, readFile as readFile2 } from "fs/promises";
|
|
2741
2823
|
import { basename, extname, isAbsolute as isAbsolute2 } from "path";
|
|
2742
|
-
import { z as
|
|
2824
|
+
import { z as z16 } from "zod";
|
|
2743
2825
|
var DEFAULT_MIME = "application/octet-stream";
|
|
2744
2826
|
var MAX_ATTACHMENT_MB = MAX_ATTACHMENT_BYTES / (1024 * 1024);
|
|
2745
2827
|
var STRUCTURED_EXTENSION_MIME = {
|
|
@@ -2839,23 +2921,23 @@ function inferMimeFromName(name) {
|
|
|
2839
2921
|
}
|
|
2840
2922
|
return DEFAULT_MIME;
|
|
2841
2923
|
}
|
|
2842
|
-
var
|
|
2843
|
-
project_id:
|
|
2844
|
-
file_path:
|
|
2924
|
+
var inputSchema16 = {
|
|
2925
|
+
project_id: z16.string().min(1).describe("UUID of the project to attach the file to."),
|
|
2926
|
+
file_path: z16.string().min(1).describe(
|
|
2845
2927
|
"Absolute path to the local file to upload. The MCP server reads this path from its own host."
|
|
2846
2928
|
),
|
|
2847
|
-
file_name:
|
|
2848
|
-
mime_type:
|
|
2929
|
+
file_name: z16.string().min(1).optional().describe("Override the filename recorded on the attachment. Defaults to the path basename."),
|
|
2930
|
+
mime_type: z16.string().min(1).optional().describe(
|
|
2849
2931
|
"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."
|
|
2850
2932
|
),
|
|
2851
|
-
override:
|
|
2933
|
+
override: z16.boolean().optional().describe(
|
|
2852
2934
|
"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)."
|
|
2853
2935
|
)
|
|
2854
2936
|
};
|
|
2855
2937
|
var uploadAttachmentTool = {
|
|
2856
2938
|
name: "upload_attachment",
|
|
2857
2939
|
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.`,
|
|
2858
|
-
inputSchema:
|
|
2940
|
+
inputSchema: inputSchema16,
|
|
2859
2941
|
handler: async (args, ctx) => {
|
|
2860
2942
|
if (!isAbsolute2(args.file_path)) {
|
|
2861
2943
|
return errorResult(`file_path must be an absolute path; got "${args.file_path}".`);
|
|
@@ -2979,7 +3061,7 @@ var uploadAttachmentTool = {
|
|
|
2979
3061
|
};
|
|
2980
3062
|
|
|
2981
3063
|
// src/server/tools/upload_spec_file.ts
|
|
2982
|
-
import { z as
|
|
3064
|
+
import { z as z17 } from "zod";
|
|
2983
3065
|
|
|
2984
3066
|
// src/server/spec-path.ts
|
|
2985
3067
|
var MAX_SPEC_DIR_DEPTH = 3;
|
|
@@ -3032,23 +3114,23 @@ var SPEC_FILE_TYPES = [
|
|
|
3032
3114
|
"proposal",
|
|
3033
3115
|
"openspec-spec"
|
|
3034
3116
|
];
|
|
3035
|
-
var
|
|
3036
|
-
project_id:
|
|
3037
|
-
file_path:
|
|
3117
|
+
var inputSchema17 = {
|
|
3118
|
+
project_id: z17.string().min(1).describe("UUID of the project to upload the file into."),
|
|
3119
|
+
file_path: z17.string().min(1).describe(
|
|
3038
3120
|
'Path of the new spec file, rooted at "specs" or "openspec" with at most 3 directory levels below the root (e.g. specs/changes/add-oauth-login/proposal.md).'
|
|
3039
3121
|
),
|
|
3040
|
-
content:
|
|
3122
|
+
content: z17.string().min(1).optional().describe(
|
|
3041
3123
|
"Full UTF-8 text content of the spec file. Provide this OR local_file_path (exactly one). Use for in-memory / generated content."
|
|
3042
3124
|
),
|
|
3043
|
-
local_file_path:
|
|
3125
|
+
local_file_path: z17.string().min(1).optional().describe(
|
|
3044
3126
|
"Path to a local file whose bytes become the spec file body; the server reads it directly. Provide this OR content (exactly one). Absolute paths are most reliable; a relative path resolves against the MCP server working directory. Point this only at an intended spec file \u2014 its bytes are uploaded to the project as-is; never use it for secrets or unrelated files."
|
|
3045
3127
|
),
|
|
3046
|
-
file_type:
|
|
3128
|
+
file_type: z17.enum(SPEC_FILE_TYPES).optional().describe("Spec file type. Omit to derive from the filename (e.g. proposal.md \u2192 proposal).")
|
|
3047
3129
|
};
|
|
3048
3130
|
var uploadSpecFileTool = {
|
|
3049
3131
|
name: "upload_spec_file",
|
|
3050
3132
|
description: "Upload a new, session-less spec file into a project. Provide the file body inline via `content` or by pointing at a local file with `local_file_path` \u2014 exactly one is required. Prefer `local_file_path` when the body is already an existing local file (the server reads it directly, avoiding sending large content through the model); use `content` for text generated in-memory. Rejects if a file already exists at the same path (use update_spec_file to overwrite).",
|
|
3051
|
-
inputSchema:
|
|
3133
|
+
inputSchema: inputSchema17,
|
|
3052
3134
|
handler: async (args, ctx) => {
|
|
3053
3135
|
const pathError = validateSpecFilePath(args.file_path);
|
|
3054
3136
|
if (pathError) {
|
|
@@ -3088,6 +3170,7 @@ function buildServer(deps) {
|
|
|
3088
3170
|
registerTool(server, getProjectTool, deps.client);
|
|
3089
3171
|
registerTool(server, getSpecFileTool, deps.client);
|
|
3090
3172
|
registerTool(server, readSpecFileTool, deps.client);
|
|
3173
|
+
registerTool(server, downloadSpecFileTool, deps.client);
|
|
3091
3174
|
registerTool(server, listSpecFileTool, deps.client);
|
|
3092
3175
|
registerTool(server, getAttachmentTool, deps.client);
|
|
3093
3176
|
registerTool(server, uploadAttachmentTool, deps.client);
|
|
@@ -3253,7 +3336,7 @@ async function canonicaliseRoot(root) {
|
|
|
3253
3336
|
import { promises as fs4 } from "fs";
|
|
3254
3337
|
import { join as join2, relative } from "path";
|
|
3255
3338
|
import { spawn } from "child_process";
|
|
3256
|
-
import { z as
|
|
3339
|
+
import { z as z18 } from "zod";
|
|
3257
3340
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
3258
3341
|
|
|
3259
3342
|
// src/reverse/ignore.ts
|
|
@@ -3558,37 +3641,37 @@ var READ_FILE_MAX_LINES = 2e3;
|
|
|
3558
3641
|
var LIST_DIR_HARD_CAP = 5e3;
|
|
3559
3642
|
var LIST_DIR_DEFAULT_CAP = 1e3;
|
|
3560
3643
|
var GREP_MAX_MATCHES_DEFAULT = 200;
|
|
3561
|
-
var listDirInput =
|
|
3562
|
-
path:
|
|
3563
|
-
recursive:
|
|
3564
|
-
maxEntries:
|
|
3565
|
-
includeIgnored:
|
|
3644
|
+
var listDirInput = z18.object({
|
|
3645
|
+
path: z18.string(),
|
|
3646
|
+
recursive: z18.boolean().optional(),
|
|
3647
|
+
maxEntries: z18.number().int().positive().optional(),
|
|
3648
|
+
includeIgnored: z18.boolean().optional()
|
|
3566
3649
|
});
|
|
3567
|
-
var readFileInput =
|
|
3568
|
-
path:
|
|
3569
|
-
offset:
|
|
3570
|
-
limit:
|
|
3571
|
-
includeIgnored:
|
|
3650
|
+
var readFileInput = z18.object({
|
|
3651
|
+
path: z18.string(),
|
|
3652
|
+
offset: z18.number().int().nonnegative().optional(),
|
|
3653
|
+
limit: z18.number().int().positive().optional(),
|
|
3654
|
+
includeIgnored: z18.boolean().optional()
|
|
3572
3655
|
});
|
|
3573
|
-
var grepInput =
|
|
3574
|
-
pattern:
|
|
3656
|
+
var grepInput = z18.object({
|
|
3657
|
+
pattern: z18.string().describe(
|
|
3575
3658
|
"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."
|
|
3576
3659
|
),
|
|
3577
|
-
path:
|
|
3578
|
-
isRegex:
|
|
3579
|
-
caseSensitive:
|
|
3580
|
-
maxMatches:
|
|
3581
|
-
contextLines:
|
|
3582
|
-
includeIgnored:
|
|
3660
|
+
path: z18.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
|
|
3661
|
+
isRegex: z18.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
|
|
3662
|
+
caseSensitive: z18.boolean().optional(),
|
|
3663
|
+
maxMatches: z18.number().int().positive().optional(),
|
|
3664
|
+
contextLines: z18.number().int().nonnegative().optional(),
|
|
3665
|
+
includeIgnored: z18.boolean().optional()
|
|
3583
3666
|
});
|
|
3584
|
-
var packCodebaseInput =
|
|
3585
|
-
subpath:
|
|
3586
|
-
includePatterns:
|
|
3587
|
-
ignorePatterns:
|
|
3667
|
+
var packCodebaseInput = z18.object({
|
|
3668
|
+
subpath: z18.string().optional(),
|
|
3669
|
+
includePatterns: z18.string().optional(),
|
|
3670
|
+
ignorePatterns: z18.string().optional()
|
|
3588
3671
|
});
|
|
3589
|
-
var packCodebaseReadPageInput =
|
|
3590
|
-
outputId:
|
|
3591
|
-
page:
|
|
3672
|
+
var packCodebaseReadPageInput = z18.object({
|
|
3673
|
+
outputId: z18.string(),
|
|
3674
|
+
page: z18.number().int().positive()
|
|
3592
3675
|
});
|
|
3593
3676
|
function structuredError(error, message, extra) {
|
|
3594
3677
|
const payload = { error, message, ...extra ?? {} };
|
|
@@ -4116,7 +4199,7 @@ function jsonSchemaFromZod(schema) {
|
|
|
4116
4199
|
return zodToJson(schema);
|
|
4117
4200
|
}
|
|
4118
4201
|
function zodToJson(schema) {
|
|
4119
|
-
if (schema instanceof
|
|
4202
|
+
if (schema instanceof z18.ZodObject) {
|
|
4120
4203
|
const shape = schema.shape;
|
|
4121
4204
|
const properties = {};
|
|
4122
4205
|
const required = [];
|
|
@@ -4131,19 +4214,19 @@ function zodToJson(schema) {
|
|
|
4131
4214
|
if (required.length > 0) out.required = required;
|
|
4132
4215
|
return out;
|
|
4133
4216
|
}
|
|
4134
|
-
if (schema instanceof
|
|
4217
|
+
if (schema instanceof z18.ZodOptional) {
|
|
4135
4218
|
return zodToJson(schema.unwrap());
|
|
4136
4219
|
}
|
|
4137
|
-
if (schema instanceof
|
|
4220
|
+
if (schema instanceof z18.ZodString) {
|
|
4138
4221
|
return { type: "string" };
|
|
4139
4222
|
}
|
|
4140
|
-
if (schema instanceof
|
|
4223
|
+
if (schema instanceof z18.ZodNumber) {
|
|
4141
4224
|
return { type: "number" };
|
|
4142
4225
|
}
|
|
4143
|
-
if (schema instanceof
|
|
4226
|
+
if (schema instanceof z18.ZodBoolean) {
|
|
4144
4227
|
return { type: "boolean" };
|
|
4145
4228
|
}
|
|
4146
|
-
if (schema instanceof
|
|
4229
|
+
if (schema instanceof z18.ZodEnum) {
|
|
4147
4230
|
return { type: "string", enum: schema.options };
|
|
4148
4231
|
}
|
|
4149
4232
|
return { type: "string" };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myspec/mcp-server",
|
|
3
|
-
"version": "0.1.4-next.
|
|
3
|
+
"version": "0.1.4-next.54",
|
|
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": {
|