@myspec/mcp-server 0.1.2-next.37 → 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 +370 -170
- 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
|
@@ -99,7 +99,7 @@ function applyCorsHeaders(res) {
|
|
|
99
99
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
100
100
|
}
|
|
101
101
|
async function waitForOAuthCallback(opts) {
|
|
102
|
-
return new Promise((
|
|
102
|
+
return new Promise((resolve3, reject) => {
|
|
103
103
|
const settledFlag = { value: false };
|
|
104
104
|
const timeoutRef = { handle: void 0 };
|
|
105
105
|
const rlRef = { handle: void 0 };
|
|
@@ -157,7 +157,7 @@ async function waitForOAuthCallback(opts) {
|
|
|
157
157
|
res.setHeader("Content-Type", "text/html");
|
|
158
158
|
res.end(SUCCESS_HTML);
|
|
159
159
|
settle(() => {
|
|
160
|
-
|
|
160
|
+
resolve3({ code });
|
|
161
161
|
});
|
|
162
162
|
});
|
|
163
163
|
server.on("error", (err) => {
|
|
@@ -204,7 +204,7 @@ async function waitForOAuthCallback(opts) {
|
|
|
204
204
|
}
|
|
205
205
|
settle(() => {
|
|
206
206
|
opts.logger("Captured pasted token; finishing sign-in...");
|
|
207
|
-
|
|
207
|
+
resolve3({ code });
|
|
208
208
|
});
|
|
209
209
|
});
|
|
210
210
|
}
|
|
@@ -348,10 +348,10 @@ function createFileCredentialsStore(paths = {}) {
|
|
|
348
348
|
async function loadOauthCreds(target) {
|
|
349
349
|
try {
|
|
350
350
|
if (process.platform !== "win32") {
|
|
351
|
-
const
|
|
352
|
-
if ((
|
|
351
|
+
const stat2 = await fs.stat(target);
|
|
352
|
+
if ((stat2.mode & 63) !== 0) {
|
|
353
353
|
throw new Error(
|
|
354
|
-
`Refusing to load credentials from ${target}: file mode is too permissive (${(
|
|
354
|
+
`Refusing to load credentials from ${target}: file mode is too permissive (${(stat2.mode & 511).toString(8)}). Run \`chmod 600 ${target}\` and retry.`
|
|
355
355
|
);
|
|
356
356
|
}
|
|
357
357
|
}
|
|
@@ -840,7 +840,7 @@ function calculateDelay(attempt, config) {
|
|
|
840
840
|
return Math.round(cappedDelay * jitterMultiplier);
|
|
841
841
|
}
|
|
842
842
|
function sleep(ms) {
|
|
843
|
-
return new Promise((
|
|
843
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
844
844
|
}
|
|
845
845
|
async function withRetry(fn, config = DEFAULT_RETRY_CONFIG, shouldRetry = defaultShouldRetry) {
|
|
846
846
|
const startTime = Date.now();
|
|
@@ -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,60 @@ 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";
|
|
2603
|
+
|
|
2604
|
+
// src/server/tools/spec-content-source.ts
|
|
2605
|
+
import { readFile, stat } from "fs/promises";
|
|
2606
|
+
import { isAbsolute, resolve } from "path";
|
|
2607
|
+
var MAX_LOCAL_SPEC_FILE_BYTES = 5 * 1024 * 1024;
|
|
2608
|
+
async function resolveSpecContent(input, maxBytes = MAX_LOCAL_SPEC_FILE_BYTES) {
|
|
2609
|
+
const { content, localFilePath } = input;
|
|
2610
|
+
if (content !== void 0 && localFilePath !== void 0) {
|
|
2611
|
+
return { error: "Provide either content or local_file_path, not both." };
|
|
2612
|
+
}
|
|
2613
|
+
if (content !== void 0) {
|
|
2614
|
+
return { bytes: new TextEncoder().encode(content) };
|
|
2615
|
+
}
|
|
2616
|
+
if (localFilePath === void 0) {
|
|
2617
|
+
return { error: "Provide the file body via content or local_file_path (one is required)." };
|
|
2618
|
+
}
|
|
2619
|
+
const absPath = isAbsolute(localFilePath) ? localFilePath : resolve(process.cwd(), localFilePath);
|
|
2620
|
+
let info;
|
|
2621
|
+
try {
|
|
2622
|
+
info = await stat(absPath);
|
|
2623
|
+
} catch (err) {
|
|
2624
|
+
const code = err.code;
|
|
2625
|
+
if (code === "ENOENT") {
|
|
2626
|
+
return { error: `local file not found: ${absPath}` };
|
|
2627
|
+
}
|
|
2628
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2629
|
+
return { error: `failed to read local file ${absPath}: ${message}` };
|
|
2630
|
+
}
|
|
2631
|
+
if (!info.isFile()) {
|
|
2632
|
+
return { error: `local_file_path is not a regular file: ${absPath}` };
|
|
2633
|
+
}
|
|
2634
|
+
if (info.size === 0) {
|
|
2635
|
+
return { error: `local file is empty: ${absPath}` };
|
|
2636
|
+
}
|
|
2637
|
+
if (info.size > maxBytes) {
|
|
2638
|
+
return {
|
|
2639
|
+
error: `local file is too large (${String(info.size)} bytes); max is ${String(maxBytes)} bytes.`
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
let raw;
|
|
2643
|
+
try {
|
|
2644
|
+
raw = await readFile(absPath);
|
|
2645
|
+
} catch (err) {
|
|
2646
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2647
|
+
return { error: `failed to read local file ${absPath}: ${message}` };
|
|
2648
|
+
}
|
|
2649
|
+
if (raw.byteLength > maxBytes) {
|
|
2650
|
+
return {
|
|
2651
|
+
error: `local file is too large (${String(raw.byteLength)} bytes); max is ${String(maxBytes)} bytes.`
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
return { bytes: new Uint8Array(raw) };
|
|
2655
|
+
}
|
|
2482
2656
|
|
|
2483
2657
|
// src/server/tools/spec-file-output.ts
|
|
2484
2658
|
function specFileSummary(file) {
|
|
@@ -2497,17 +2671,29 @@ function specFileSummary(file) {
|
|
|
2497
2671
|
}
|
|
2498
2672
|
|
|
2499
2673
|
// src/server/tools/update_spec_file.ts
|
|
2500
|
-
var
|
|
2501
|
-
file_id:
|
|
2502
|
-
project_id:
|
|
2503
|
-
file_path:
|
|
2504
|
-
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(
|
|
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."
|
|
2680
|
+
),
|
|
2681
|
+
local_file_path: z14.string().min(1).optional().describe(
|
|
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."
|
|
2683
|
+
)
|
|
2505
2684
|
};
|
|
2506
2685
|
var updateSpecFileTool = {
|
|
2507
2686
|
name: "update_spec_file",
|
|
2508
|
-
description: "Overwrite an existing spec file by saving a new revision
|
|
2509
|
-
inputSchema:
|
|
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).",
|
|
2688
|
+
inputSchema: inputSchema14,
|
|
2510
2689
|
handler: async (args, ctx) => {
|
|
2690
|
+
const resolved = await resolveSpecContent({
|
|
2691
|
+
content: args.content,
|
|
2692
|
+
localFilePath: args.local_file_path
|
|
2693
|
+
});
|
|
2694
|
+
if (resolved.error || !resolved.bytes) {
|
|
2695
|
+
return errorResult(resolved.error ?? "Failed to resolve file content.");
|
|
2696
|
+
}
|
|
2511
2697
|
let fileId;
|
|
2512
2698
|
if (args.file_id) {
|
|
2513
2699
|
const meta = await ctx.client.getFileMetadata(args.file_id).catch(() => null);
|
|
@@ -2526,17 +2712,16 @@ var updateSpecFileTool = {
|
|
|
2526
2712
|
} else {
|
|
2527
2713
|
return errorResult("Provide either file_id, or both project_id and file_path.");
|
|
2528
2714
|
}
|
|
2529
|
-
const
|
|
2530
|
-
const file = await ctx.client.saveSpecFileRevision(fileId, content);
|
|
2715
|
+
const file = await ctx.client.saveSpecFileRevision(fileId, resolved.bytes);
|
|
2531
2716
|
return jsonResult({ file: specFileSummary(file) });
|
|
2532
2717
|
}
|
|
2533
2718
|
};
|
|
2534
2719
|
|
|
2535
2720
|
// src/server/tools/upload_attachment.ts
|
|
2536
2721
|
import { createHash as createHash2 } from "crypto";
|
|
2537
|
-
import { lstat, readFile } from "fs/promises";
|
|
2538
|
-
import { basename, extname, isAbsolute } from "path";
|
|
2539
|
-
import { z as
|
|
2722
|
+
import { lstat, readFile as readFile2 } from "fs/promises";
|
|
2723
|
+
import { basename, extname, isAbsolute as isAbsolute2 } from "path";
|
|
2724
|
+
import { z as z15 } from "zod";
|
|
2540
2725
|
var DEFAULT_MIME = "application/octet-stream";
|
|
2541
2726
|
var MAX_ATTACHMENT_MB = MAX_ATTACHMENT_BYTES / (1024 * 1024);
|
|
2542
2727
|
var STRUCTURED_EXTENSION_MIME = {
|
|
@@ -2636,25 +2821,25 @@ function inferMimeFromName(name) {
|
|
|
2636
2821
|
}
|
|
2637
2822
|
return DEFAULT_MIME;
|
|
2638
2823
|
}
|
|
2639
|
-
var
|
|
2640
|
-
project_id:
|
|
2641
|
-
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(
|
|
2642
2827
|
"Absolute path to the local file to upload. The MCP server reads this path from its own host."
|
|
2643
2828
|
),
|
|
2644
|
-
file_name:
|
|
2645
|
-
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(
|
|
2646
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."
|
|
2647
2832
|
),
|
|
2648
|
-
override:
|
|
2833
|
+
override: z15.boolean().optional().describe(
|
|
2649
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)."
|
|
2650
2835
|
)
|
|
2651
2836
|
};
|
|
2652
2837
|
var uploadAttachmentTool = {
|
|
2653
2838
|
name: "upload_attachment",
|
|
2654
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.`,
|
|
2655
|
-
inputSchema:
|
|
2840
|
+
inputSchema: inputSchema15,
|
|
2656
2841
|
handler: async (args, ctx) => {
|
|
2657
|
-
if (!
|
|
2842
|
+
if (!isAbsolute2(args.file_path)) {
|
|
2658
2843
|
return errorResult(`file_path must be an absolute path; got "${args.file_path}".`);
|
|
2659
2844
|
}
|
|
2660
2845
|
let stats;
|
|
@@ -2690,7 +2875,7 @@ var uploadAttachmentTool = {
|
|
|
2690
2875
|
}
|
|
2691
2876
|
let buf;
|
|
2692
2877
|
try {
|
|
2693
|
-
buf = await
|
|
2878
|
+
buf = await readFile2(args.file_path);
|
|
2694
2879
|
} catch (err) {
|
|
2695
2880
|
const code = err.code;
|
|
2696
2881
|
if (code === "EACCES" || code === "EPERM") {
|
|
@@ -2776,7 +2961,7 @@ var uploadAttachmentTool = {
|
|
|
2776
2961
|
};
|
|
2777
2962
|
|
|
2778
2963
|
// src/server/tools/upload_spec_file.ts
|
|
2779
|
-
import { z as
|
|
2964
|
+
import { z as z16 } from "zod";
|
|
2780
2965
|
|
|
2781
2966
|
// src/server/spec-path.ts
|
|
2782
2967
|
var MAX_SPEC_DIR_DEPTH = 3;
|
|
@@ -2794,15 +2979,16 @@ function validateSpecFilePath(filePath) {
|
|
|
2794
2979
|
if (segments.some((s) => s === "" || s === ".")) {
|
|
2795
2980
|
return 'file_path must be a normalized path (no empty, "." or trailing-slash segments).';
|
|
2796
2981
|
}
|
|
2797
|
-
|
|
2798
|
-
|
|
2982
|
+
const root = segments[0];
|
|
2983
|
+
if (root !== "specs" && root !== "openspec") {
|
|
2984
|
+
return "file_path must be rooted at 'specs' or 'openspec' (e.g. specs/changes/add-oauth-login/proposal.md).";
|
|
2799
2985
|
}
|
|
2800
2986
|
if (segments.length < 2) {
|
|
2801
|
-
return
|
|
2987
|
+
return `file_path must include a filename under '${root}' (e.g. ${root}/proposal.md).`;
|
|
2802
2988
|
}
|
|
2803
2989
|
const dirLevels = segments.length - 2;
|
|
2804
2990
|
if (dirLevels > MAX_SPEC_DIR_DEPTH) {
|
|
2805
|
-
return `file_path must not nest deeper than ${String(MAX_SPEC_DIR_DEPTH)} directory levels below
|
|
2991
|
+
return `file_path must not nest deeper than ${String(MAX_SPEC_DIR_DEPTH)} directory levels below the '${root}' root.`;
|
|
2806
2992
|
}
|
|
2807
2993
|
return null;
|
|
2808
2994
|
}
|
|
@@ -2828,23 +3014,35 @@ var SPEC_FILE_TYPES = [
|
|
|
2828
3014
|
"proposal",
|
|
2829
3015
|
"openspec-spec"
|
|
2830
3016
|
];
|
|
2831
|
-
var
|
|
2832
|
-
project_id:
|
|
2833
|
-
file_path:
|
|
2834
|
-
'Path of the new spec file, rooted at "specs" with at most 3 directory levels below
|
|
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(
|
|
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).'
|
|
2835
3021
|
),
|
|
2836
|
-
content:
|
|
2837
|
-
|
|
3022
|
+
content: z16.string().min(1).optional().describe(
|
|
3023
|
+
"Full UTF-8 text content of the spec file. Provide this OR local_file_path (exactly one). Use for in-memory / generated content."
|
|
3024
|
+
),
|
|
3025
|
+
local_file_path: z16.string().min(1).optional().describe(
|
|
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."
|
|
3027
|
+
),
|
|
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).")
|
|
2838
3029
|
};
|
|
2839
3030
|
var uploadSpecFileTool = {
|
|
2840
3031
|
name: "upload_spec_file",
|
|
2841
|
-
description: "Upload a new, session-less spec file into a project
|
|
2842
|
-
inputSchema:
|
|
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).",
|
|
3033
|
+
inputSchema: inputSchema16,
|
|
2843
3034
|
handler: async (args, ctx) => {
|
|
2844
3035
|
const pathError = validateSpecFilePath(args.file_path);
|
|
2845
3036
|
if (pathError) {
|
|
2846
3037
|
return errorResult(pathError);
|
|
2847
3038
|
}
|
|
3039
|
+
const resolved = await resolveSpecContent({
|
|
3040
|
+
content: args.content,
|
|
3041
|
+
localFilePath: args.local_file_path
|
|
3042
|
+
});
|
|
3043
|
+
if (resolved.error || !resolved.bytes) {
|
|
3044
|
+
return errorResult(resolved.error ?? "Failed to resolve file content.");
|
|
3045
|
+
}
|
|
2848
3046
|
const existing = await ctx.client.findSpecFileByPath(args.project_id, args.file_path);
|
|
2849
3047
|
if (existing) {
|
|
2850
3048
|
return errorResult(
|
|
@@ -2852,12 +3050,11 @@ var uploadSpecFileTool = {
|
|
|
2852
3050
|
);
|
|
2853
3051
|
}
|
|
2854
3052
|
const fileType = args.file_type ?? deriveFileType(args.file_path);
|
|
2855
|
-
const content = new TextEncoder().encode(args.content);
|
|
2856
3053
|
const file = await ctx.client.uploadSpecFile({
|
|
2857
3054
|
projectId: args.project_id,
|
|
2858
3055
|
filePath: args.file_path,
|
|
2859
3056
|
fileType,
|
|
2860
|
-
content
|
|
3057
|
+
content: resolved.bytes
|
|
2861
3058
|
});
|
|
2862
3059
|
return jsonResult({ file: specFileSummary(file) });
|
|
2863
3060
|
}
|
|
@@ -2871,8 +3068,9 @@ function buildServer(deps) {
|
|
|
2871
3068
|
});
|
|
2872
3069
|
registerTool(server, listProjectsTool, deps.client);
|
|
2873
3070
|
registerTool(server, getProjectTool, deps.client);
|
|
2874
|
-
registerTool(server,
|
|
2875
|
-
registerTool(server,
|
|
3071
|
+
registerTool(server, getSpecFileTool, deps.client);
|
|
3072
|
+
registerTool(server, readSpecFileTool, deps.client);
|
|
3073
|
+
registerTool(server, listSpecFileTool, deps.client);
|
|
2876
3074
|
registerTool(server, getAttachmentTool, deps.client);
|
|
2877
3075
|
registerTool(server, uploadAttachmentTool, deps.client);
|
|
2878
3076
|
registerTool(server, createProjectTool, deps.client);
|
|
@@ -2882,6 +3080,8 @@ function buildServer(deps) {
|
|
|
2882
3080
|
registerTool(server, deleteProjectTool, deps.client);
|
|
2883
3081
|
registerTool(server, uploadSpecFileTool, deps.client);
|
|
2884
3082
|
registerTool(server, updateSpecFileTool, deps.client);
|
|
3083
|
+
registerTool(server, moveSpecFileToTrashTool, deps.client);
|
|
3084
|
+
registerTool(server, restoreSpecFileFromTrashTool, deps.client);
|
|
2885
3085
|
return server;
|
|
2886
3086
|
}
|
|
2887
3087
|
async function startStdioServer(deps) {
|
|
@@ -2949,13 +3149,13 @@ var WebSocketClientTransport = class {
|
|
|
2949
3149
|
return Promise.resolve();
|
|
2950
3150
|
}
|
|
2951
3151
|
send(message) {
|
|
2952
|
-
return new Promise((
|
|
3152
|
+
return new Promise((resolve3, reject) => {
|
|
2953
3153
|
this.ws.send(JSON.stringify(message), (err) => {
|
|
2954
3154
|
if (err) {
|
|
2955
3155
|
reject(err);
|
|
2956
3156
|
return;
|
|
2957
3157
|
}
|
|
2958
|
-
|
|
3158
|
+
resolve3();
|
|
2959
3159
|
});
|
|
2960
3160
|
});
|
|
2961
3161
|
}
|
|
@@ -2989,7 +3189,7 @@ var WebSocketClientTransport = class {
|
|
|
2989
3189
|
|
|
2990
3190
|
// src/reverse/safe-path.ts
|
|
2991
3191
|
import { realpath } from "fs/promises";
|
|
2992
|
-
import { isAbsolute as
|
|
3192
|
+
import { isAbsolute as isAbsolute3, resolve as resolve2, sep } from "path";
|
|
2993
3193
|
var PathOutsideRootError = class extends Error {
|
|
2994
3194
|
constructor(userPath) {
|
|
2995
3195
|
super(`path_outside_root: ${userPath}`);
|
|
@@ -3007,10 +3207,10 @@ var AbsolutePathError = class extends Error {
|
|
|
3007
3207
|
userPath;
|
|
3008
3208
|
};
|
|
3009
3209
|
async function resolveSafePath(realRoot, userPath, opts = {}) {
|
|
3010
|
-
if (
|
|
3210
|
+
if (isAbsolute3(userPath)) {
|
|
3011
3211
|
throw new AbsolutePathError(userPath);
|
|
3012
3212
|
}
|
|
3013
|
-
const joined =
|
|
3213
|
+
const joined = resolve2(realRoot, userPath);
|
|
3014
3214
|
let resolved;
|
|
3015
3215
|
try {
|
|
3016
3216
|
resolved = await realpath(joined);
|
|
@@ -3035,7 +3235,7 @@ async function canonicaliseRoot(root) {
|
|
|
3035
3235
|
import { promises as fs4 } from "fs";
|
|
3036
3236
|
import { join as join2, relative } from "path";
|
|
3037
3237
|
import { spawn } from "child_process";
|
|
3038
|
-
import { z as
|
|
3238
|
+
import { z as z17 } from "zod";
|
|
3039
3239
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
3040
3240
|
|
|
3041
3241
|
// src/reverse/ignore.ts
|
|
@@ -3115,7 +3315,7 @@ function ripgrepIgnoreGlobs() {
|
|
|
3115
3315
|
|
|
3116
3316
|
// src/reverse/pack-codebase.ts
|
|
3117
3317
|
import { createHash as createHash3 } from "crypto";
|
|
3118
|
-
import { mkdtemp, readFile as
|
|
3318
|
+
import { mkdtemp, readFile as readFile3, rm } from "fs/promises";
|
|
3119
3319
|
import { tmpdir } from "os";
|
|
3120
3320
|
import { join } from "path";
|
|
3121
3321
|
import { runCli } from "repomix";
|
|
@@ -3252,7 +3452,7 @@ async function invokeRepomix(exec, targetDir, args, timeoutMs) {
|
|
|
3252
3452
|
const execPromise = exec(["."], targetDir, options);
|
|
3253
3453
|
try {
|
|
3254
3454
|
await withTimeout(execPromise, timeoutMs);
|
|
3255
|
-
const content = await
|
|
3455
|
+
const content = await readFile3(outputFile, "utf-8");
|
|
3256
3456
|
await rm(workDir, { recursive: true, force: true });
|
|
3257
3457
|
return content;
|
|
3258
3458
|
} catch (err) {
|
|
@@ -3285,37 +3485,37 @@ var READ_FILE_MAX_LINES = 2e3;
|
|
|
3285
3485
|
var LIST_DIR_HARD_CAP = 5e3;
|
|
3286
3486
|
var LIST_DIR_DEFAULT_CAP = 1e3;
|
|
3287
3487
|
var GREP_MAX_MATCHES_DEFAULT = 200;
|
|
3288
|
-
var listDirInput =
|
|
3289
|
-
path:
|
|
3290
|
-
recursive:
|
|
3291
|
-
maxEntries:
|
|
3292
|
-
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()
|
|
3293
3493
|
});
|
|
3294
|
-
var readFileInput =
|
|
3295
|
-
path:
|
|
3296
|
-
offset:
|
|
3297
|
-
limit:
|
|
3298
|
-
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()
|
|
3299
3499
|
});
|
|
3300
|
-
var grepInput =
|
|
3301
|
-
pattern:
|
|
3500
|
+
var grepInput = z17.object({
|
|
3501
|
+
pattern: z17.string().describe(
|
|
3302
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."
|
|
3303
3503
|
),
|
|
3304
|
-
path:
|
|
3305
|
-
isRegex:
|
|
3306
|
-
caseSensitive:
|
|
3307
|
-
maxMatches:
|
|
3308
|
-
contextLines:
|
|
3309
|
-
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()
|
|
3310
3510
|
});
|
|
3311
|
-
var packCodebaseInput =
|
|
3312
|
-
subpath:
|
|
3313
|
-
includePatterns:
|
|
3314
|
-
ignorePatterns:
|
|
3511
|
+
var packCodebaseInput = z17.object({
|
|
3512
|
+
subpath: z17.string().optional(),
|
|
3513
|
+
includePatterns: z17.string().optional(),
|
|
3514
|
+
ignorePatterns: z17.string().optional()
|
|
3315
3515
|
});
|
|
3316
|
-
var packCodebaseReadPageInput =
|
|
3317
|
-
outputId:
|
|
3318
|
-
page:
|
|
3516
|
+
var packCodebaseReadPageInput = z17.object({
|
|
3517
|
+
outputId: z17.string(),
|
|
3518
|
+
page: z17.number().int().positive()
|
|
3319
3519
|
});
|
|
3320
3520
|
function structuredError(error, message, extra) {
|
|
3321
3521
|
const payload = { error, message, ...extra ?? {} };
|
|
@@ -3402,20 +3602,20 @@ async function handleRead(args, opts) {
|
|
|
3402
3602
|
return structuredError("path_ignored", `Path matches ignore rule ${check.rule ?? ""}`);
|
|
3403
3603
|
}
|
|
3404
3604
|
}
|
|
3405
|
-
let
|
|
3605
|
+
let stat2;
|
|
3406
3606
|
try {
|
|
3407
|
-
|
|
3607
|
+
stat2 = await fs4.stat(realPath);
|
|
3408
3608
|
} catch (err) {
|
|
3409
3609
|
return structuredError("stat_failed", errorMessage(err));
|
|
3410
3610
|
}
|
|
3411
|
-
if (!
|
|
3611
|
+
if (!stat2.isFile()) {
|
|
3412
3612
|
return structuredError("not_a_file", `Not a regular file: ${args.path}`);
|
|
3413
3613
|
}
|
|
3414
|
-
if (
|
|
3614
|
+
if (stat2.size > READ_FILE_MAX_BYTES) {
|
|
3415
3615
|
return structuredError(
|
|
3416
3616
|
"file_too_large",
|
|
3417
|
-
`File is ${String(
|
|
3418
|
-
{ sizeBytes:
|
|
3617
|
+
`File is ${String(stat2.size)} bytes; the read cap is ${String(READ_FILE_MAX_BYTES)} bytes. Use local_fs_grep to find the lines you need, or local_pack_codebase to pull the whole tree.`,
|
|
3618
|
+
{ sizeBytes: stat2.size, maxBytes: READ_FILE_MAX_BYTES }
|
|
3419
3619
|
);
|
|
3420
3620
|
}
|
|
3421
3621
|
let raw;
|
|
@@ -3843,7 +4043,7 @@ function jsonSchemaFromZod(schema) {
|
|
|
3843
4043
|
return zodToJson(schema);
|
|
3844
4044
|
}
|
|
3845
4045
|
function zodToJson(schema) {
|
|
3846
|
-
if (schema instanceof
|
|
4046
|
+
if (schema instanceof z17.ZodObject) {
|
|
3847
4047
|
const shape = schema.shape;
|
|
3848
4048
|
const properties = {};
|
|
3849
4049
|
const required = [];
|
|
@@ -3858,19 +4058,19 @@ function zodToJson(schema) {
|
|
|
3858
4058
|
if (required.length > 0) out.required = required;
|
|
3859
4059
|
return out;
|
|
3860
4060
|
}
|
|
3861
|
-
if (schema instanceof
|
|
4061
|
+
if (schema instanceof z17.ZodOptional) {
|
|
3862
4062
|
return zodToJson(schema.unwrap());
|
|
3863
4063
|
}
|
|
3864
|
-
if (schema instanceof
|
|
4064
|
+
if (schema instanceof z17.ZodString) {
|
|
3865
4065
|
return { type: "string" };
|
|
3866
4066
|
}
|
|
3867
|
-
if (schema instanceof
|
|
4067
|
+
if (schema instanceof z17.ZodNumber) {
|
|
3868
4068
|
return { type: "number" };
|
|
3869
4069
|
}
|
|
3870
|
-
if (schema instanceof
|
|
4070
|
+
if (schema instanceof z17.ZodBoolean) {
|
|
3871
4071
|
return { type: "boolean" };
|
|
3872
4072
|
}
|
|
3873
|
-
if (schema instanceof
|
|
4073
|
+
if (schema instanceof z17.ZodEnum) {
|
|
3874
4074
|
return { type: "string", enum: schema.options };
|
|
3875
4075
|
}
|
|
3876
4076
|
return { type: "string" };
|
|
@@ -4020,7 +4220,7 @@ async function runOneConnection(opts) {
|
|
|
4020
4220
|
pongTimer = null;
|
|
4021
4221
|
}
|
|
4022
4222
|
});
|
|
4023
|
-
return new Promise((
|
|
4223
|
+
return new Promise((resolve3) => {
|
|
4024
4224
|
ws.once("close", (code, reasonBuf) => {
|
|
4025
4225
|
clearInterval(pingTimer);
|
|
4026
4226
|
if (pongTimer) clearTimeout(pongTimer);
|
|
@@ -4030,7 +4230,7 @@ async function runOneConnection(opts) {
|
|
|
4030
4230
|
`
|
|
4031
4231
|
);
|
|
4032
4232
|
const terminal = TERMINAL_CLOSE_CODES.has(code);
|
|
4033
|
-
|
|
4233
|
+
resolve3({
|
|
4034
4234
|
terminal,
|
|
4035
4235
|
reason: `code=${String(code)} ${reasonStr}`,
|
|
4036
4236
|
upMs: Date.now() - connectedAt,
|
|
@@ -4040,10 +4240,10 @@ async function runOneConnection(opts) {
|
|
|
4040
4240
|
});
|
|
4041
4241
|
}
|
|
4042
4242
|
function waitForOpen(ws) {
|
|
4043
|
-
return new Promise((
|
|
4243
|
+
return new Promise((resolve3, reject) => {
|
|
4044
4244
|
const onOpen = () => {
|
|
4045
4245
|
ws.removeListener("error", onError);
|
|
4046
|
-
|
|
4246
|
+
resolve3();
|
|
4047
4247
|
};
|
|
4048
4248
|
const onError = (err) => {
|
|
4049
4249
|
ws.removeListener("open", onOpen);
|
|
@@ -4054,7 +4254,7 @@ function waitForOpen(ws) {
|
|
|
4054
4254
|
});
|
|
4055
4255
|
}
|
|
4056
4256
|
function sleep2(ms) {
|
|
4057
|
-
return new Promise((
|
|
4257
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
4058
4258
|
}
|
|
4059
4259
|
|
|
4060
4260
|
// src/index.ts
|
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": {
|