@myspec/mcp-server 0.1.2-next.38 → 0.1.2-next.39
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 +7 -4
- package/dist/index.js +256 -132
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ Local state is stored under `~/.myspec/` (alongside the on-disk cache root), spl
|
|
|
80
80
|
|---|---|
|
|
81
81
|
| `MYSPEC_USER_AUTH_URL` | user-auth base URL (default `https://auth.myspec.dev`). The webapp URL is derived from it; the platform / ai-agent URLs are discovered via the webapp. |
|
|
82
82
|
| `MYSPEC_REFRESH_TOKEN` | Skip file-based credentials; the server mints a fresh access token on first use via this refresh token. The supported way to wire the MCP server up without running `npx @myspec/mcp-server login`. |
|
|
83
|
-
| `MYSPEC_DOWNLOAD_ROOT` | Absolute path used by `
|
|
83
|
+
| `MYSPEC_DOWNLOAD_ROOT` | Absolute path used by `read_spec_file` as its on-disk cache root (default: `~/.myspec`). Tools never write outside this root. |
|
|
84
84
|
| `MYSPEC_AI_AGENT_WS_URL` | ai-agent WebSocket URL for `reverse`; skips the webapp discovery call |
|
|
85
85
|
|
|
86
86
|
## Tools
|
|
@@ -89,14 +89,17 @@ Local state is stored under `~/.myspec/` (alongside the on-disk cache root), spl
|
|
|
89
89
|
|---|---|---|
|
|
90
90
|
| `list_projects` | `query?`, `limit?`, `offset?` | Paginated list of projects |
|
|
91
91
|
| `get_project` | `project_id` | Project + active sessions + files + attachments |
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
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` |
|
|
94
|
+
| `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
|
+
| `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
|
+
| `restore_spec_file_from_trash` | `file_id` | Restores a trashed spec file back to the active file list, reversing `move_spec_file_to_trash` |
|
|
94
97
|
| `get_attachment` | `attachment_id`, optional `include_download_url` | Attachment metadata (looked up org-scoped by id); with `include_download_url>0` also returns a signed download URL |
|
|
95
98
|
| `upload_attachment` | `project_id`, `file_path` (absolute), optional `file_name`, optional `mime_type`, optional `override` | Uploads the local file as a new attachment on the project. Returns `attachment_id`, `file_name` (the **effective** name the platform stored — may differ from the requested one when auto-dedup adds a `(N)` suffix), `has_file_name_conflict` (`true` when stored name ≠ requested name), `mime_type`, `file_size_bytes`, `checksum` (format: `sha256:<hex>`), `file_uri`, `reused_existing_attachment` (`true` when an existing attachment with the same name AND same checksum was found and returned without re-uploading; `file_uri` is then omitted because the list endpoint does not surface it — call `get_attachment` for a download URL). With `override=true`, any existing attachment whose recorded name matches is soft-deleted first (returned as `overridden_attachment_ids` — empty array means nothing matched) and the new upload keeps the requested name; the same-checksum short-circuit still applies, so a no-op re-upload never deletes anything. Max 10 MB. Supported content: PDF, DOCX, XLSX, and any UTF-8 text file (XML, JSON, YAML, HTML, Markdown, CSV, source code, ...) |
|
|
96
99
|
|
|
97
100
|
### `include_download_url` semantics
|
|
98
101
|
|
|
99
|
-
`
|
|
102
|
+
`get_spec_file`:
|
|
100
103
|
|
|
101
104
|
- Omit or `0` → metadata only.
|
|
102
105
|
- Equal to current `revision_count` → adds `download_url` (signed) and `expires_at`.
|
package/dist/index.js
CHANGED
|
@@ -1890,6 +1890,26 @@ var PlatformClient = class {
|
|
|
1890
1890
|
async deleteProject(projectId) {
|
|
1891
1891
|
await this.withTokenRetry((jwt, c) => c.project.deleteProject(projectId, jwt));
|
|
1892
1892
|
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Moves a spec file to the Trash Bin (recoverable). The platform hides a
|
|
1895
|
+
* trashed file from the default file list and all AI-agent tools, but keeps
|
|
1896
|
+
* it restorable indefinitely via {@link restoreFile}.
|
|
1897
|
+
*/
|
|
1898
|
+
async trashFile(fileId) {
|
|
1899
|
+
await this.withTokenRetry((jwt, c) => c.file.trashFile(fileId, jwt));
|
|
1900
|
+
}
|
|
1901
|
+
/** Restores a spec file from the Trash Bin back to the active file list. */
|
|
1902
|
+
async restoreFile(fileId) {
|
|
1903
|
+
await this.withTokenRetry((jwt, c) => c.file.restoreFile(fileId, jwt));
|
|
1904
|
+
}
|
|
1905
|
+
/**
|
|
1906
|
+
* Lists every spec file currently in the project's Trash Bin. The platform
|
|
1907
|
+
* returns all trashed files in one response (no pagination), so this is a
|
|
1908
|
+
* single call rather than a paged loop like {@link listFiles}.
|
|
1909
|
+
*/
|
|
1910
|
+
async listTrashedFiles(projectId) {
|
|
1911
|
+
return this.withTokenRetry((jwt, c) => c.file.listTrashedFiles(projectId, jwt));
|
|
1912
|
+
}
|
|
1893
1913
|
/**
|
|
1894
1914
|
* Two-phase upload of a new, session-less ("unbound") spec file. Reserves a
|
|
1895
1915
|
* file id (Phase 1, `sessionId` omitted so the file is unbound), then PUTs
|
|
@@ -2131,61 +2151,15 @@ var getAttachmentTool = {
|
|
|
2131
2151
|
}
|
|
2132
2152
|
};
|
|
2133
2153
|
|
|
2134
|
-
// src/server/tools/
|
|
2154
|
+
// src/server/tools/get_project.ts
|
|
2135
2155
|
import { z as z5 } from "zod";
|
|
2136
2156
|
var inputSchema5 = {
|
|
2137
|
-
|
|
2138
|
-
include_download_url: z5.number().int().nonnegative().optional().describe(
|
|
2139
|
-
"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."
|
|
2140
|
-
)
|
|
2141
|
-
};
|
|
2142
|
-
var getFileTool = {
|
|
2143
|
-
name: "get_file",
|
|
2144
|
-
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.",
|
|
2145
|
-
inputSchema: inputSchema5,
|
|
2146
|
-
handler: async (args, ctx) => {
|
|
2147
|
-
const meta = await ctx.client.getFileMetadata(args.file_id);
|
|
2148
|
-
const base = {
|
|
2149
|
-
id: meta.id,
|
|
2150
|
-
project_id: meta.projectId,
|
|
2151
|
-
session_id: meta.sessionId,
|
|
2152
|
-
file_type: meta.fileType,
|
|
2153
|
-
file_path: meta.filePath,
|
|
2154
|
-
file_size_bytes: meta.fileSizeBytes,
|
|
2155
|
-
checksum: meta.checksum,
|
|
2156
|
-
revision_count: meta.revisionCount,
|
|
2157
|
-
created_at: meta.createdAt,
|
|
2158
|
-
updated_at: meta.updatedAt
|
|
2159
|
-
};
|
|
2160
|
-
const requested = args.include_download_url ?? 0;
|
|
2161
|
-
if (requested <= 0) {
|
|
2162
|
-
return jsonResult(base);
|
|
2163
|
-
}
|
|
2164
|
-
if (requested > meta.revisionCount) {
|
|
2165
|
-
return errorResult(
|
|
2166
|
-
`include_download_url=${String(requested)} is greater than revision_count=${String(meta.revisionCount)} for file ${meta.id}.`
|
|
2167
|
-
);
|
|
2168
|
-
}
|
|
2169
|
-
const isLatest = requested === meta.revisionCount;
|
|
2170
|
-
const signed = isLatest ? await ctx.client.getFileDownloadUrl(meta.id) : await ctx.client.getRevisionDownloadUrl(meta.id, requested);
|
|
2171
|
-
const downloadBlock = {
|
|
2172
|
-
download_url: signed.signedUrl,
|
|
2173
|
-
download_revision: requested,
|
|
2174
|
-
expires_at: signed.expiresAt.toISOString()
|
|
2175
|
-
};
|
|
2176
|
-
return jsonResult({ ...base, ...downloadBlock });
|
|
2177
|
-
}
|
|
2178
|
-
};
|
|
2179
|
-
|
|
2180
|
-
// src/server/tools/get_project.ts
|
|
2181
|
-
import { z as z6 } from "zod";
|
|
2182
|
-
var inputSchema6 = {
|
|
2183
|
-
project_id: z6.string().min(1).describe("UUID of the project to fetch")
|
|
2157
|
+
project_id: z5.string().min(1).describe("UUID of the project to fetch")
|
|
2184
2158
|
};
|
|
2185
2159
|
var getProjectTool = {
|
|
2186
2160
|
name: "get_project",
|
|
2187
2161
|
description: "Get details for a project including all active spec sessions, files (id + metadata), and attachments (id + metadata).",
|
|
2188
|
-
inputSchema:
|
|
2162
|
+
inputSchema: inputSchema5,
|
|
2189
2163
|
handler: async (args, ctx) => {
|
|
2190
2164
|
const [project, sessions, files, attachments] = await Promise.all([
|
|
2191
2165
|
ctx.client.getProject(args.project_id),
|
|
@@ -2230,6 +2204,52 @@ var getProjectTool = {
|
|
|
2230
2204
|
}
|
|
2231
2205
|
};
|
|
2232
2206
|
|
|
2207
|
+
// src/server/tools/get_spec_file.ts
|
|
2208
|
+
import { z as z6 } from "zod";
|
|
2209
|
+
var inputSchema6 = {
|
|
2210
|
+
file_id: z6.string().min(1).describe("UUID of the file"),
|
|
2211
|
+
include_download_url: z6.number().int().nonnegative().optional().describe(
|
|
2212
|
+
"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."
|
|
2213
|
+
)
|
|
2214
|
+
};
|
|
2215
|
+
var getSpecFileTool = {
|
|
2216
|
+
name: "get_spec_file",
|
|
2217
|
+
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.",
|
|
2218
|
+
inputSchema: inputSchema6,
|
|
2219
|
+
handler: async (args, ctx) => {
|
|
2220
|
+
const meta = await ctx.client.getFileMetadata(args.file_id);
|
|
2221
|
+
const base = {
|
|
2222
|
+
id: meta.id,
|
|
2223
|
+
project_id: meta.projectId,
|
|
2224
|
+
session_id: meta.sessionId,
|
|
2225
|
+
file_type: meta.fileType,
|
|
2226
|
+
file_path: meta.filePath,
|
|
2227
|
+
file_size_bytes: meta.fileSizeBytes,
|
|
2228
|
+
checksum: meta.checksum,
|
|
2229
|
+
revision_count: meta.revisionCount,
|
|
2230
|
+
created_at: meta.createdAt,
|
|
2231
|
+
updated_at: meta.updatedAt
|
|
2232
|
+
};
|
|
2233
|
+
const requested = args.include_download_url ?? 0;
|
|
2234
|
+
if (requested <= 0) {
|
|
2235
|
+
return jsonResult(base);
|
|
2236
|
+
}
|
|
2237
|
+
if (requested > meta.revisionCount) {
|
|
2238
|
+
return errorResult(
|
|
2239
|
+
`include_download_url=${String(requested)} is greater than revision_count=${String(meta.revisionCount)} for file ${meta.id}.`
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
const isLatest = requested === meta.revisionCount;
|
|
2243
|
+
const signed = isLatest ? await ctx.client.getFileDownloadUrl(meta.id) : await ctx.client.getRevisionDownloadUrl(meta.id, requested);
|
|
2244
|
+
const downloadBlock = {
|
|
2245
|
+
download_url: signed.signedUrl,
|
|
2246
|
+
download_revision: requested,
|
|
2247
|
+
expires_at: signed.expiresAt.toISOString()
|
|
2248
|
+
};
|
|
2249
|
+
return jsonResult({ ...base, ...downloadBlock });
|
|
2250
|
+
}
|
|
2251
|
+
};
|
|
2252
|
+
|
|
2233
2253
|
// src/server/tools/list_projects.ts
|
|
2234
2254
|
import { z as z7 } from "zod";
|
|
2235
2255
|
var inputSchema7 = {
|
|
@@ -2264,11 +2284,97 @@ var listProjectsTool = {
|
|
|
2264
2284
|
}
|
|
2265
2285
|
};
|
|
2266
2286
|
|
|
2267
|
-
// src/server/tools/
|
|
2287
|
+
// src/server/tools/list_spec_file.ts
|
|
2288
|
+
import { z as z8 } from "zod";
|
|
2289
|
+
var PAGE_SIZE = 100;
|
|
2290
|
+
var inputSchema8 = {
|
|
2291
|
+
project_id: z8.string().min(1).describe("UUID of the project whose spec files to list."),
|
|
2292
|
+
path: z8.string().optional().describe(
|
|
2293
|
+
'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.'
|
|
2294
|
+
),
|
|
2295
|
+
trashed: z8.boolean().optional().describe(
|
|
2296
|
+
"When true, list files in the project Trash Bin instead. When false or omitted, list only live files (trashed files excluded)."
|
|
2297
|
+
)
|
|
2298
|
+
};
|
|
2299
|
+
var listSpecFileTool = {
|
|
2300
|
+
name: "list_spec_file",
|
|
2301
|
+
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.',
|
|
2302
|
+
inputSchema: inputSchema8,
|
|
2303
|
+
handler: async (args, ctx) => {
|
|
2304
|
+
const trashed = args.trashed ?? false;
|
|
2305
|
+
const files = trashed ? await ctx.client.listTrashedFiles(args.project_id) : await listAllLiveFiles(ctx.client, args.project_id);
|
|
2306
|
+
const prefix = normalizePrefix(args.path);
|
|
2307
|
+
const matches = files.filter((f) => isUnderPath(f.filePath, prefix)).map(toEntry).sort((a, b) => a.file_path.localeCompare(b.file_path));
|
|
2308
|
+
return jsonResult({
|
|
2309
|
+
project_id: args.project_id,
|
|
2310
|
+
path: prefix,
|
|
2311
|
+
trashed,
|
|
2312
|
+
total: matches.length,
|
|
2313
|
+
files: matches
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
};
|
|
2317
|
+
async function listAllLiveFiles(client, projectId) {
|
|
2318
|
+
const all = [];
|
|
2319
|
+
let offset = 0;
|
|
2320
|
+
for (; ; ) {
|
|
2321
|
+
const page = await client.listFiles(projectId, { limit: PAGE_SIZE, offset });
|
|
2322
|
+
all.push(...page.files);
|
|
2323
|
+
offset += page.files.length;
|
|
2324
|
+
if (page.files.length < PAGE_SIZE || offset >= page.total) {
|
|
2325
|
+
return all;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
function toEntry(file) {
|
|
2330
|
+
const entry = {
|
|
2331
|
+
file_id: file.id,
|
|
2332
|
+
file_path: file.filePath,
|
|
2333
|
+
file_type: file.fileType,
|
|
2334
|
+
file_size_bytes: file.fileSizeBytes,
|
|
2335
|
+
revision_count: file.revisionCount,
|
|
2336
|
+
session_id: file.sessionId,
|
|
2337
|
+
updated_at: file.updatedAt
|
|
2338
|
+
};
|
|
2339
|
+
if (file.trashedAt) {
|
|
2340
|
+
entry.trashed_at = file.trashedAt;
|
|
2341
|
+
}
|
|
2342
|
+
return entry;
|
|
2343
|
+
}
|
|
2344
|
+
function normalizePrefix(path4) {
|
|
2345
|
+
if (!path4) {
|
|
2346
|
+
return "";
|
|
2347
|
+
}
|
|
2348
|
+
const trimmed = path4.trim().replace(/^\/+/, "").replace(/\/+$/, "");
|
|
2349
|
+
return trimmed;
|
|
2350
|
+
}
|
|
2351
|
+
function isUnderPath(filePath, prefix) {
|
|
2352
|
+
if (prefix === "") {
|
|
2353
|
+
return true;
|
|
2354
|
+
}
|
|
2355
|
+
return filePath === prefix || filePath.startsWith(prefix + "/");
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
// src/server/tools/move_spec_file_to_trash.ts
|
|
2359
|
+
import { z as z9 } from "zod";
|
|
2360
|
+
var inputSchema9 = {
|
|
2361
|
+
file_id: z9.string().min(1).describe("UUID of the spec file to move to the Trash Bin.")
|
|
2362
|
+
};
|
|
2363
|
+
var moveSpecFileToTrashTool = {
|
|
2364
|
+
name: "move_spec_file_to_trash",
|
|
2365
|
+
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.",
|
|
2366
|
+
inputSchema: inputSchema9,
|
|
2367
|
+
handler: async (args, ctx) => {
|
|
2368
|
+
await ctx.client.trashFile(args.file_id);
|
|
2369
|
+
return jsonResult({ trashed: true, file_id: args.file_id });
|
|
2370
|
+
}
|
|
2371
|
+
};
|
|
2372
|
+
|
|
2373
|
+
// src/server/tools/read_spec_file.ts
|
|
2268
2374
|
import { promises as fs3 } from "fs";
|
|
2269
2375
|
import os3 from "os";
|
|
2270
2376
|
import path3 from "path";
|
|
2271
|
-
import { z as
|
|
2377
|
+
import { z as z10 } from "zod";
|
|
2272
2378
|
|
|
2273
2379
|
// src/server/download-path.ts
|
|
2274
2380
|
import { promises as fs2 } from "fs";
|
|
@@ -2333,16 +2439,16 @@ async function writeBytesAtomically(target, bytes) {
|
|
|
2333
2439
|
await fs2.rename(tmp, target);
|
|
2334
2440
|
}
|
|
2335
2441
|
|
|
2336
|
-
// src/server/tools/
|
|
2442
|
+
// src/server/tools/read_spec_file.ts
|
|
2337
2443
|
var MAX_READ_BYTES = 1 * 1024 * 1024;
|
|
2338
|
-
var
|
|
2339
|
-
file_id:
|
|
2340
|
-
revision:
|
|
2444
|
+
var inputSchema10 = {
|
|
2445
|
+
file_id: z10.string().min(1).describe("UUID of the file"),
|
|
2446
|
+
revision: z10.number().int().positive().optional().describe("Specific revision number to read (defaults to the latest revision).")
|
|
2341
2447
|
};
|
|
2342
|
-
var
|
|
2343
|
-
name: "
|
|
2344
|
-
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
|
|
2345
|
-
inputSchema:
|
|
2448
|
+
var readSpecFileTool = {
|
|
2449
|
+
name: "read_spec_file",
|
|
2450
|
+
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 get_spec_file with `include_download_url` to obtain a download URL for those.",
|
|
2451
|
+
inputSchema: inputSchema10,
|
|
2346
2452
|
handler: async (args, ctx) => {
|
|
2347
2453
|
const meta = await ctx.client.getFileMetadata(args.file_id);
|
|
2348
2454
|
const targetRevision = args.revision ?? meta.revisionCount;
|
|
@@ -2360,7 +2466,7 @@ var readFileTool = {
|
|
|
2360
2466
|
const escape = assertWithinRoot(cachePath, cacheRoot);
|
|
2361
2467
|
if (escape) {
|
|
2362
2468
|
return errorResult(
|
|
2363
|
-
`
|
|
2469
|
+
`read_spec_file refused to write cache: ${escape}. This indicates an unexpected platform response shape (projectId/fileId/filePath).`
|
|
2364
2470
|
);
|
|
2365
2471
|
}
|
|
2366
2472
|
let bytes;
|
|
@@ -2381,7 +2487,7 @@ var readFileTool = {
|
|
|
2381
2487
|
}
|
|
2382
2488
|
if (isBinary(bytes)) {
|
|
2383
2489
|
return errorResult(
|
|
2384
|
-
`File ${args.file_id} appears to be binary (file_type=${meta.fileType}, size=${String(bytes.byteLength)} bytes). Use
|
|
2490
|
+
`File ${args.file_id} appears to be binary (file_type=${meta.fileType}, size=${String(bytes.byteLength)} bytes). Use get_spec_file with \`include_download_url\` to obtain a download URL.`
|
|
2385
2491
|
);
|
|
2386
2492
|
}
|
|
2387
2493
|
let text;
|
|
@@ -2389,7 +2495,7 @@ var readFileTool = {
|
|
|
2389
2495
|
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
2390
2496
|
} catch {
|
|
2391
2497
|
return errorResult(
|
|
2392
|
-
`File ${args.file_id} is not valid UTF-8 text. Use
|
|
2498
|
+
`File ${args.file_id} is not valid UTF-8 text. Use get_spec_file with \`include_download_url\` to obtain a download URL.`
|
|
2393
2499
|
);
|
|
2394
2500
|
}
|
|
2395
2501
|
return jsonResult({
|
|
@@ -2404,7 +2510,7 @@ var readFileTool = {
|
|
|
2404
2510
|
}
|
|
2405
2511
|
};
|
|
2406
2512
|
function buildOversizeMessage(fileId, byteLength) {
|
|
2407
|
-
return `File ${fileId} is ${String(byteLength)} bytes which exceeds the read cap of ${String(MAX_READ_BYTES)} bytes. Use
|
|
2513
|
+
return `File ${fileId} is ${String(byteLength)} bytes which exceeds the read cap of ${String(MAX_READ_BYTES)} bytes. Use get_spec_file with \`include_download_url\` to obtain a download URL and fetch it directly.`;
|
|
2408
2514
|
}
|
|
2409
2515
|
function maskHomedir(p, home = os3.homedir()) {
|
|
2410
2516
|
if (!home) {
|
|
@@ -2427,15 +2533,30 @@ function isBinary(bytes) {
|
|
|
2427
2533
|
return false;
|
|
2428
2534
|
}
|
|
2429
2535
|
|
|
2536
|
+
// src/server/tools/restore_spec_file_from_trash.ts
|
|
2537
|
+
import { z as z11 } from "zod";
|
|
2538
|
+
var inputSchema11 = {
|
|
2539
|
+
file_id: z11.string().min(1).describe("UUID of the spec file to restore from the Trash Bin.")
|
|
2540
|
+
};
|
|
2541
|
+
var restoreSpecFileFromTrashTool = {
|
|
2542
|
+
name: "restore_spec_file_from_trash",
|
|
2543
|
+
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.",
|
|
2544
|
+
inputSchema: inputSchema11,
|
|
2545
|
+
handler: async (args, ctx) => {
|
|
2546
|
+
await ctx.client.restoreFile(args.file_id);
|
|
2547
|
+
return jsonResult({ restored: true, file_id: args.file_id });
|
|
2548
|
+
}
|
|
2549
|
+
};
|
|
2550
|
+
|
|
2430
2551
|
// src/server/tools/unarchive_project.ts
|
|
2431
|
-
import { z as
|
|
2432
|
-
var
|
|
2433
|
-
project_id:
|
|
2552
|
+
import { z as z12 } from "zod";
|
|
2553
|
+
var inputSchema12 = {
|
|
2554
|
+
project_id: z12.string().min(1).describe("UUID of the project to unarchive.")
|
|
2434
2555
|
};
|
|
2435
2556
|
var unarchiveProjectTool = {
|
|
2436
2557
|
name: "unarchive_project",
|
|
2437
2558
|
description: 'Unarchive a project (sets status to "active"). Idempotent: unarchiving an already-active project succeeds.',
|
|
2438
|
-
inputSchema:
|
|
2559
|
+
inputSchema: inputSchema12,
|
|
2439
2560
|
handler: async (args, ctx) => {
|
|
2440
2561
|
const project = await ctx.client.unarchiveProject(args.project_id);
|
|
2441
2562
|
return jsonResult({ project: projectSummary(project) });
|
|
@@ -2443,19 +2564,19 @@ var unarchiveProjectTool = {
|
|
|
2443
2564
|
};
|
|
2444
2565
|
|
|
2445
2566
|
// src/server/tools/update_project.ts
|
|
2446
|
-
import { z as
|
|
2447
|
-
var
|
|
2448
|
-
project_id:
|
|
2449
|
-
name:
|
|
2450
|
-
description:
|
|
2451
|
-
project_type:
|
|
2567
|
+
import { z as z13 } from "zod";
|
|
2568
|
+
var inputSchema13 = {
|
|
2569
|
+
project_id: z13.string().min(1).describe("UUID of the project to update."),
|
|
2570
|
+
name: z13.string().min(1).optional().describe("New project name."),
|
|
2571
|
+
description: z13.string().optional().describe("New project description."),
|
|
2572
|
+
project_type: z13.enum(["greenfield", "brownfield", "auto"]).optional().describe(
|
|
2452
2573
|
"Codebase type. 'greenfield' or 'brownfield' sets it explicitly; 'auto' clears it so the workflow decides. Omit to leave unchanged."
|
|
2453
2574
|
)
|
|
2454
2575
|
};
|
|
2455
2576
|
var updateProjectTool = {
|
|
2456
2577
|
name: "update_project",
|
|
2457
2578
|
description: "Update a project's metadata (name, description, project_type). Returns the updated metadata.",
|
|
2458
|
-
inputSchema:
|
|
2579
|
+
inputSchema: inputSchema13,
|
|
2459
2580
|
handler: async (args, ctx) => {
|
|
2460
2581
|
if (args.name === void 0 && args.description === void 0 && args.project_type === void 0) {
|
|
2461
2582
|
return errorResult(
|
|
@@ -2478,7 +2599,7 @@ var updateProjectTool = {
|
|
|
2478
2599
|
};
|
|
2479
2600
|
|
|
2480
2601
|
// src/server/tools/update_spec_file.ts
|
|
2481
|
-
import { z as
|
|
2602
|
+
import { z as z14 } from "zod";
|
|
2482
2603
|
|
|
2483
2604
|
// src/server/tools/spec-content-source.ts
|
|
2484
2605
|
import { readFile, stat } from "fs/promises";
|
|
@@ -2550,21 +2671,21 @@ function specFileSummary(file) {
|
|
|
2550
2671
|
}
|
|
2551
2672
|
|
|
2552
2673
|
// src/server/tools/update_spec_file.ts
|
|
2553
|
-
var
|
|
2554
|
-
file_id:
|
|
2555
|
-
project_id:
|
|
2556
|
-
file_path:
|
|
2557
|
-
content:
|
|
2674
|
+
var inputSchema14 = {
|
|
2675
|
+
file_id: z14.string().min(1).optional().describe("UUID of the spec file to overwrite. Provide this or (project_id + file_path)."),
|
|
2676
|
+
project_id: z14.string().min(1).optional().describe("UUID of the project. Required (with file_path) when file_id is not given."),
|
|
2677
|
+
file_path: z14.string().min(1).optional().describe("Path of the spec file. Required (with project_id) when file_id is not given."),
|
|
2678
|
+
content: z14.string().min(1).optional().describe(
|
|
2558
2679
|
"Full UTF-8 replacement content, saved as a new revision. Provide this OR local_file_path (exactly one). Use for in-memory / generated content."
|
|
2559
2680
|
),
|
|
2560
|
-
local_file_path:
|
|
2681
|
+
local_file_path: z14.string().min(1).optional().describe(
|
|
2561
2682
|
"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."
|
|
2562
2683
|
)
|
|
2563
2684
|
};
|
|
2564
2685
|
var updateSpecFileTool = {
|
|
2565
2686
|
name: "update_spec_file",
|
|
2566
2687
|
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).",
|
|
2567
|
-
inputSchema:
|
|
2688
|
+
inputSchema: inputSchema14,
|
|
2568
2689
|
handler: async (args, ctx) => {
|
|
2569
2690
|
const resolved = await resolveSpecContent({
|
|
2570
2691
|
content: args.content,
|
|
@@ -2600,7 +2721,7 @@ var updateSpecFileTool = {
|
|
|
2600
2721
|
import { createHash as createHash2 } from "crypto";
|
|
2601
2722
|
import { lstat, readFile as readFile2 } from "fs/promises";
|
|
2602
2723
|
import { basename, extname, isAbsolute as isAbsolute2 } from "path";
|
|
2603
|
-
import { z as
|
|
2724
|
+
import { z as z15 } from "zod";
|
|
2604
2725
|
var DEFAULT_MIME = "application/octet-stream";
|
|
2605
2726
|
var MAX_ATTACHMENT_MB = MAX_ATTACHMENT_BYTES / (1024 * 1024);
|
|
2606
2727
|
var STRUCTURED_EXTENSION_MIME = {
|
|
@@ -2700,23 +2821,23 @@ function inferMimeFromName(name) {
|
|
|
2700
2821
|
}
|
|
2701
2822
|
return DEFAULT_MIME;
|
|
2702
2823
|
}
|
|
2703
|
-
var
|
|
2704
|
-
project_id:
|
|
2705
|
-
file_path:
|
|
2824
|
+
var inputSchema15 = {
|
|
2825
|
+
project_id: z15.string().min(1).describe("UUID of the project to attach the file to."),
|
|
2826
|
+
file_path: z15.string().min(1).describe(
|
|
2706
2827
|
"Absolute path to the local file to upload. The MCP server reads this path from its own host."
|
|
2707
2828
|
),
|
|
2708
|
-
file_name:
|
|
2709
|
-
mime_type:
|
|
2829
|
+
file_name: z15.string().min(1).optional().describe("Override the filename recorded on the attachment. Defaults to the path basename."),
|
|
2830
|
+
mime_type: z15.string().min(1).optional().describe(
|
|
2710
2831
|
"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."
|
|
2711
2832
|
),
|
|
2712
|
-
override:
|
|
2833
|
+
override: z15.boolean().optional().describe(
|
|
2713
2834
|
"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)."
|
|
2714
2835
|
)
|
|
2715
2836
|
};
|
|
2716
2837
|
var uploadAttachmentTool = {
|
|
2717
2838
|
name: "upload_attachment",
|
|
2718
2839
|
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.`,
|
|
2719
|
-
inputSchema:
|
|
2840
|
+
inputSchema: inputSchema15,
|
|
2720
2841
|
handler: async (args, ctx) => {
|
|
2721
2842
|
if (!isAbsolute2(args.file_path)) {
|
|
2722
2843
|
return errorResult(`file_path must be an absolute path; got "${args.file_path}".`);
|
|
@@ -2840,7 +2961,7 @@ var uploadAttachmentTool = {
|
|
|
2840
2961
|
};
|
|
2841
2962
|
|
|
2842
2963
|
// src/server/tools/upload_spec_file.ts
|
|
2843
|
-
import { z as
|
|
2964
|
+
import { z as z16 } from "zod";
|
|
2844
2965
|
|
|
2845
2966
|
// src/server/spec-path.ts
|
|
2846
2967
|
var MAX_SPEC_DIR_DEPTH = 3;
|
|
@@ -2893,23 +3014,23 @@ var SPEC_FILE_TYPES = [
|
|
|
2893
3014
|
"proposal",
|
|
2894
3015
|
"openspec-spec"
|
|
2895
3016
|
];
|
|
2896
|
-
var
|
|
2897
|
-
project_id:
|
|
2898
|
-
file_path:
|
|
3017
|
+
var inputSchema16 = {
|
|
3018
|
+
project_id: z16.string().min(1).describe("UUID of the project to upload the file into."),
|
|
3019
|
+
file_path: z16.string().min(1).describe(
|
|
2899
3020
|
'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).'
|
|
2900
3021
|
),
|
|
2901
|
-
content:
|
|
3022
|
+
content: z16.string().min(1).optional().describe(
|
|
2902
3023
|
"Full UTF-8 text content of the spec file. Provide this OR local_file_path (exactly one). Use for in-memory / generated content."
|
|
2903
3024
|
),
|
|
2904
|
-
local_file_path:
|
|
3025
|
+
local_file_path: z16.string().min(1).optional().describe(
|
|
2905
3026
|
"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."
|
|
2906
3027
|
),
|
|
2907
|
-
file_type:
|
|
3028
|
+
file_type: z16.enum(SPEC_FILE_TYPES).optional().describe("Spec file type. Omit to derive from the filename (e.g. proposal.md \u2192 proposal).")
|
|
2908
3029
|
};
|
|
2909
3030
|
var uploadSpecFileTool = {
|
|
2910
3031
|
name: "upload_spec_file",
|
|
2911
3032
|
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).",
|
|
2912
|
-
inputSchema:
|
|
3033
|
+
inputSchema: inputSchema16,
|
|
2913
3034
|
handler: async (args, ctx) => {
|
|
2914
3035
|
const pathError = validateSpecFilePath(args.file_path);
|
|
2915
3036
|
if (pathError) {
|
|
@@ -2947,8 +3068,9 @@ function buildServer(deps) {
|
|
|
2947
3068
|
});
|
|
2948
3069
|
registerTool(server, listProjectsTool, deps.client);
|
|
2949
3070
|
registerTool(server, getProjectTool, deps.client);
|
|
2950
|
-
registerTool(server,
|
|
2951
|
-
registerTool(server,
|
|
3071
|
+
registerTool(server, getSpecFileTool, deps.client);
|
|
3072
|
+
registerTool(server, readSpecFileTool, deps.client);
|
|
3073
|
+
registerTool(server, listSpecFileTool, deps.client);
|
|
2952
3074
|
registerTool(server, getAttachmentTool, deps.client);
|
|
2953
3075
|
registerTool(server, uploadAttachmentTool, deps.client);
|
|
2954
3076
|
registerTool(server, createProjectTool, deps.client);
|
|
@@ -2958,6 +3080,8 @@ function buildServer(deps) {
|
|
|
2958
3080
|
registerTool(server, deleteProjectTool, deps.client);
|
|
2959
3081
|
registerTool(server, uploadSpecFileTool, deps.client);
|
|
2960
3082
|
registerTool(server, updateSpecFileTool, deps.client);
|
|
3083
|
+
registerTool(server, moveSpecFileToTrashTool, deps.client);
|
|
3084
|
+
registerTool(server, restoreSpecFileFromTrashTool, deps.client);
|
|
2961
3085
|
return server;
|
|
2962
3086
|
}
|
|
2963
3087
|
async function startStdioServer(deps) {
|
|
@@ -3111,7 +3235,7 @@ async function canonicaliseRoot(root) {
|
|
|
3111
3235
|
import { promises as fs4 } from "fs";
|
|
3112
3236
|
import { join as join2, relative } from "path";
|
|
3113
3237
|
import { spawn } from "child_process";
|
|
3114
|
-
import { z as
|
|
3238
|
+
import { z as z17 } from "zod";
|
|
3115
3239
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
3116
3240
|
|
|
3117
3241
|
// src/reverse/ignore.ts
|
|
@@ -3361,37 +3485,37 @@ var READ_FILE_MAX_LINES = 2e3;
|
|
|
3361
3485
|
var LIST_DIR_HARD_CAP = 5e3;
|
|
3362
3486
|
var LIST_DIR_DEFAULT_CAP = 1e3;
|
|
3363
3487
|
var GREP_MAX_MATCHES_DEFAULT = 200;
|
|
3364
|
-
var listDirInput =
|
|
3365
|
-
path:
|
|
3366
|
-
recursive:
|
|
3367
|
-
maxEntries:
|
|
3368
|
-
includeIgnored:
|
|
3488
|
+
var listDirInput = z17.object({
|
|
3489
|
+
path: z17.string(),
|
|
3490
|
+
recursive: z17.boolean().optional(),
|
|
3491
|
+
maxEntries: z17.number().int().positive().optional(),
|
|
3492
|
+
includeIgnored: z17.boolean().optional()
|
|
3369
3493
|
});
|
|
3370
|
-
var readFileInput =
|
|
3371
|
-
path:
|
|
3372
|
-
offset:
|
|
3373
|
-
limit:
|
|
3374
|
-
includeIgnored:
|
|
3494
|
+
var readFileInput = z17.object({
|
|
3495
|
+
path: z17.string(),
|
|
3496
|
+
offset: z17.number().int().nonnegative().optional(),
|
|
3497
|
+
limit: z17.number().int().positive().optional(),
|
|
3498
|
+
includeIgnored: z17.boolean().optional()
|
|
3375
3499
|
});
|
|
3376
|
-
var grepInput =
|
|
3377
|
-
pattern:
|
|
3500
|
+
var grepInput = z17.object({
|
|
3501
|
+
pattern: z17.string().describe(
|
|
3378
3502
|
"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."
|
|
3379
3503
|
),
|
|
3380
|
-
path:
|
|
3381
|
-
isRegex:
|
|
3382
|
-
caseSensitive:
|
|
3383
|
-
maxMatches:
|
|
3384
|
-
contextLines:
|
|
3385
|
-
includeIgnored:
|
|
3504
|
+
path: z17.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
|
|
3505
|
+
isRegex: z17.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
|
|
3506
|
+
caseSensitive: z17.boolean().optional(),
|
|
3507
|
+
maxMatches: z17.number().int().positive().optional(),
|
|
3508
|
+
contextLines: z17.number().int().nonnegative().optional(),
|
|
3509
|
+
includeIgnored: z17.boolean().optional()
|
|
3386
3510
|
});
|
|
3387
|
-
var packCodebaseInput =
|
|
3388
|
-
subpath:
|
|
3389
|
-
includePatterns:
|
|
3390
|
-
ignorePatterns:
|
|
3511
|
+
var packCodebaseInput = z17.object({
|
|
3512
|
+
subpath: z17.string().optional(),
|
|
3513
|
+
includePatterns: z17.string().optional(),
|
|
3514
|
+
ignorePatterns: z17.string().optional()
|
|
3391
3515
|
});
|
|
3392
|
-
var packCodebaseReadPageInput =
|
|
3393
|
-
outputId:
|
|
3394
|
-
page:
|
|
3516
|
+
var packCodebaseReadPageInput = z17.object({
|
|
3517
|
+
outputId: z17.string(),
|
|
3518
|
+
page: z17.number().int().positive()
|
|
3395
3519
|
});
|
|
3396
3520
|
function structuredError(error, message, extra) {
|
|
3397
3521
|
const payload = { error, message, ...extra ?? {} };
|
|
@@ -3919,7 +4043,7 @@ function jsonSchemaFromZod(schema) {
|
|
|
3919
4043
|
return zodToJson(schema);
|
|
3920
4044
|
}
|
|
3921
4045
|
function zodToJson(schema) {
|
|
3922
|
-
if (schema instanceof
|
|
4046
|
+
if (schema instanceof z17.ZodObject) {
|
|
3923
4047
|
const shape = schema.shape;
|
|
3924
4048
|
const properties = {};
|
|
3925
4049
|
const required = [];
|
|
@@ -3934,19 +4058,19 @@ function zodToJson(schema) {
|
|
|
3934
4058
|
if (required.length > 0) out.required = required;
|
|
3935
4059
|
return out;
|
|
3936
4060
|
}
|
|
3937
|
-
if (schema instanceof
|
|
4061
|
+
if (schema instanceof z17.ZodOptional) {
|
|
3938
4062
|
return zodToJson(schema.unwrap());
|
|
3939
4063
|
}
|
|
3940
|
-
if (schema instanceof
|
|
4064
|
+
if (schema instanceof z17.ZodString) {
|
|
3941
4065
|
return { type: "string" };
|
|
3942
4066
|
}
|
|
3943
|
-
if (schema instanceof
|
|
4067
|
+
if (schema instanceof z17.ZodNumber) {
|
|
3944
4068
|
return { type: "number" };
|
|
3945
4069
|
}
|
|
3946
|
-
if (schema instanceof
|
|
4070
|
+
if (schema instanceof z17.ZodBoolean) {
|
|
3947
4071
|
return { type: "boolean" };
|
|
3948
4072
|
}
|
|
3949
|
-
if (schema instanceof
|
|
4073
|
+
if (schema instanceof z17.ZodEnum) {
|
|
3950
4074
|
return { type: "string", enum: schema.options };
|
|
3951
4075
|
}
|
|
3952
4076
|
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.39",
|
|
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": {
|