@asad120414/mcpcall 0.1.0

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.
Files changed (3) hide show
  1. package/README.md +36 -0
  2. package/dist/index.js +1396 -0
  3. package/package.json +35 -0
package/dist/index.js ADDED
@@ -0,0 +1,1396 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import {
7
+ CallToolRequestSchema,
8
+ ListToolsRequestSchema
9
+ } from "@modelcontextprotocol/sdk/types.js";
10
+ import { z as z4 } from "zod";
11
+
12
+ // ../shared/src/schema.ts
13
+ import { z } from "zod";
14
+ var workspaceSlugSchema = z.string().min(1).max(64).regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, "lowercase letters, digits, hyphens; no leading/trailing hyphen");
15
+ var filePathSchema = z.string().min(1).max(512).refine((p) => !p.startsWith("/") && !p.includes("\\"), "must be relative POSIX path").refine((p) => p.split("/").every((seg) => seg !== "" && seg !== "." && seg !== ".."), "no traversal segments").refine((p) => !p.endsWith("/"), "no trailing slash");
16
+ var fileKindSchema = z.enum(["claude_md", "agent", "skill", "command", "rule", "other"]);
17
+ var workspaceSchema = z.object({
18
+ id: z.string().uuid(),
19
+ ownerUserId: z.string().uuid(),
20
+ slug: workspaceSlugSchema,
21
+ name: z.string().min(1).max(120),
22
+ description: z.string().max(500).nullable(),
23
+ createdAt: z.string().datetime(),
24
+ updatedAt: z.string().datetime()
25
+ });
26
+ var fileSchema = z.object({
27
+ id: z.string().uuid(),
28
+ workspaceId: z.string().uuid(),
29
+ path: filePathSchema,
30
+ kind: fileKindSchema,
31
+ content: z.string(),
32
+ sizeBytes: z.number().int().nonnegative(),
33
+ updatedAt: z.string().datetime()
34
+ });
35
+ var fileManifestEntrySchema = z.object({
36
+ path: filePathSchema,
37
+ kind: fileKindSchema,
38
+ sizeBytes: z.number().int().nonnegative(),
39
+ sha256: z.string().length(64),
40
+ updatedAt: z.string().datetime()
41
+ });
42
+ var apiKeyPublicSchema = z.object({
43
+ id: z.string().uuid(),
44
+ name: z.string().min(1).max(80),
45
+ prefix: z.string(),
46
+ lastUsedAt: z.string().datetime().nullable(),
47
+ createdAt: z.string().datetime(),
48
+ revokedAt: z.string().datetime().nullable()
49
+ });
50
+ var createWorkspaceRequestSchema = z.object({
51
+ slug: workspaceSlugSchema,
52
+ name: z.string().min(1).max(120),
53
+ description: z.string().max(500).optional()
54
+ });
55
+ var updateWorkspaceRequestSchema = z.object({
56
+ name: z.string().min(1).max(120).optional(),
57
+ description: z.string().max(500).nullable().optional()
58
+ });
59
+ var upsertFileRequestSchema = z.object({
60
+ content: z.string().max(2e6),
61
+ kind: fileKindSchema.optional()
62
+ });
63
+ var workspaceManifestSchema = z.object({
64
+ workspace: workspaceSchema,
65
+ files: z.array(fileManifestEntrySchema)
66
+ });
67
+ var redeemInviteRequestSchema = z.object({
68
+ code: z.string().min(1).max(64)
69
+ });
70
+ var createApiKeyRequestSchema = z.object({
71
+ name: z.string().min(1).max(80)
72
+ });
73
+ var createApiKeyResponseSchema = apiKeyPublicSchema.extend({
74
+ /** Full secret; shown once at creation, never again. Format: `mc_live_<prefix>_<secret>`. */
75
+ token: z.string()
76
+ });
77
+ var apiErrorSchema = z.object({
78
+ error: z.object({
79
+ code: z.string(),
80
+ message: z.string(),
81
+ details: z.unknown().optional()
82
+ })
83
+ });
84
+
85
+ // ../shared/src/paths.ts
86
+ function projectRelativeWritePath(workspaceRelPath) {
87
+ const path11 = workspaceRelPath.replace(/^\.\//, "");
88
+ if (path11 === "CLAUDE.md" || path11 === "RULES.md") return path11;
89
+ return `.claude/${path11}`;
90
+ }
91
+ function topLevelExcludePaths(workspaceRelPaths) {
92
+ const top = /* @__PURE__ */ new Set();
93
+ for (const p of workspaceRelPaths) {
94
+ const written = projectRelativeWritePath(p);
95
+ const first = written.split("/")[0];
96
+ if (first) top.add(first.endsWith("/") ? first : first.includes(".") ? first : `${first}/`);
97
+ }
98
+ return [...top].sort();
99
+ }
100
+
101
+ // src/api-client.ts
102
+ import { z as z2 } from "zod";
103
+ var DEFAULT_API_URL = "https://mcpcall.dev";
104
+ var ApiError = class extends Error {
105
+ status;
106
+ code;
107
+ details;
108
+ constructor(status2, code, message, details) {
109
+ super(message);
110
+ this.name = "ApiError";
111
+ this.status = status2;
112
+ this.code = code;
113
+ this.details = details;
114
+ }
115
+ };
116
+ var fileContentResponseSchema = z2.object({
117
+ content: z2.string()
118
+ });
119
+ var listWorkspacesResponseSchema = z2.object({
120
+ workspaces: z2.array(workspaceSchema)
121
+ });
122
+ var ApiClient = class {
123
+ apiUrl;
124
+ apiKey;
125
+ constructor(options = {}) {
126
+ const apiKey = options.apiKey ?? process.env.MCPCALL_API_KEY;
127
+ if (!apiKey) {
128
+ throw new Error(
129
+ "MCPCALL_API_KEY is not set. Add it to your shell rc \u2014 do NOT put it in .mcp.json."
130
+ );
131
+ }
132
+ this.apiKey = apiKey;
133
+ const url = options.apiUrl ?? process.env.MCPCALL_API_URL ?? DEFAULT_API_URL;
134
+ this.apiUrl = url.replace(/\/+$/, "");
135
+ }
136
+ async request(method, pathname, body) {
137
+ const url = `${this.apiUrl}${pathname}`;
138
+ const headers = {
139
+ Authorization: `Bearer ${this.apiKey}`,
140
+ Accept: "application/json"
141
+ };
142
+ let init = { method, headers };
143
+ if (body !== void 0) {
144
+ headers["Content-Type"] = "application/json";
145
+ init = { ...init, body: JSON.stringify(body) };
146
+ }
147
+ let res;
148
+ try {
149
+ res = await fetch(url, init);
150
+ } catch (err2) {
151
+ throw new ApiError(0, "network_error", `Network error reaching ${url}: ${err2.message}`);
152
+ }
153
+ const text = await res.text();
154
+ if (res.status < 200 || res.status >= 300) {
155
+ let code = "http_error";
156
+ let message = `HTTP ${res.status} for ${method} ${pathname}`;
157
+ let details;
158
+ if (text) {
159
+ try {
160
+ const json = JSON.parse(text);
161
+ const parsed = apiErrorSchema.safeParse(json);
162
+ if (parsed.success) {
163
+ code = parsed.data.error.code;
164
+ message = parsed.data.error.message;
165
+ details = parsed.data.error.details;
166
+ } else {
167
+ details = json;
168
+ }
169
+ } catch {
170
+ details = text;
171
+ }
172
+ }
173
+ throw new ApiError(res.status, code, message, details);
174
+ }
175
+ return { status: res.status, text, contentType: res.headers.get("content-type") };
176
+ }
177
+ parseJson(text, schema, ctx) {
178
+ let json;
179
+ try {
180
+ json = JSON.parse(text);
181
+ } catch (err2) {
182
+ throw new ApiError(0, "invalid_response", `Invalid JSON from ${ctx}: ${err2.message}`);
183
+ }
184
+ const parsed = schema.safeParse(json);
185
+ if (!parsed.success) {
186
+ throw new ApiError(0, "schema_error", `Response from ${ctx} failed validation: ${parsed.error.message}`, parsed.error.flatten());
187
+ }
188
+ return parsed.data;
189
+ }
190
+ async listWorkspaces() {
191
+ const { text } = await this.request("GET", "/api/mcp/v1/workspaces");
192
+ const parsed = this.parseJson(text, listWorkspacesResponseSchema, "GET /workspaces");
193
+ return parsed.workspaces;
194
+ }
195
+ async listFiles(slug) {
196
+ const { text } = await this.request("GET", `/api/mcp/v1/workspaces/${encodeURIComponent(slug)}/files`);
197
+ return this.parseJson(text, workspaceManifestSchema, `GET /workspaces/${slug}/files`);
198
+ }
199
+ async getFileContent(slug, filePath) {
200
+ const encoded = filePath.split("/").map((seg) => encodeURIComponent(seg)).join("/");
201
+ const { text } = await this.request(
202
+ "GET",
203
+ `/api/mcp/v1/workspaces/${encodeURIComponent(slug)}/files/${encoded}`
204
+ );
205
+ return this.parseJson(text, fileContentResponseSchema, `GET file ${filePath}`).content;
206
+ }
207
+ async putFile(slug, filePath, content, kind) {
208
+ const encoded = filePath.split("/").map((seg) => encodeURIComponent(seg)).join("/");
209
+ const body = { content };
210
+ if (kind !== void 0) {
211
+ body.kind = fileKindSchema.parse(kind);
212
+ }
213
+ await this.request(
214
+ "PUT",
215
+ `/api/mcp/v1/workspaces/${encodeURIComponent(slug)}/files/${encoded}`,
216
+ body
217
+ );
218
+ }
219
+ async deleteFile(slug, filePath) {
220
+ const encoded = filePath.split("/").map((seg) => encodeURIComponent(seg)).join("/");
221
+ await this.request(
222
+ "DELETE",
223
+ `/api/mcp/v1/workspaces/${encodeURIComponent(slug)}/files/${encoded}`
224
+ );
225
+ }
226
+ };
227
+
228
+ // src/tools/listWorkspaces.ts
229
+ async function listWorkspaces(api) {
230
+ const ws = await api.listWorkspaces();
231
+ return { workspaces: ws.map(toSummary) };
232
+ }
233
+ function toSummary(w) {
234
+ return {
235
+ id: w.id,
236
+ slug: w.slug,
237
+ name: w.name,
238
+ description: w.description,
239
+ updatedAt: w.updatedAt
240
+ };
241
+ }
242
+
243
+ // src/tools/listFiles.ts
244
+ async function listFiles(api, slug) {
245
+ const manifest = await api.listFiles(slug);
246
+ return {
247
+ workspace: {
248
+ id: manifest.workspace.id,
249
+ slug: manifest.workspace.slug,
250
+ name: manifest.workspace.name
251
+ },
252
+ files: manifest.files.map((f) => ({
253
+ path: f.path,
254
+ kind: f.kind,
255
+ sizeBytes: f.sizeBytes,
256
+ sha256: f.sha256,
257
+ updatedAt: f.updatedAt
258
+ }))
259
+ };
260
+ }
261
+
262
+ // src/tools/pullWorkspace.ts
263
+ import { promises as fs4 } from "node:fs";
264
+ import path4 from "node:path";
265
+
266
+ // src/git/findGitDir.ts
267
+ import { promises as fs } from "node:fs";
268
+ import path from "node:path";
269
+ async function statSafe(p) {
270
+ try {
271
+ return await fs.stat(p);
272
+ } catch {
273
+ return null;
274
+ }
275
+ }
276
+ async function findGitDir(start) {
277
+ let dir = path.resolve(start);
278
+ const seen = /* @__PURE__ */ new Set();
279
+ while (!seen.has(dir)) {
280
+ seen.add(dir);
281
+ const dotGit = path.join(dir, ".git");
282
+ const st = await statSafe(dotGit);
283
+ if (st) {
284
+ if (st.isDirectory()) {
285
+ return { workTree: dir, gitDir: dotGit };
286
+ }
287
+ if (st.isFile()) {
288
+ const contents = await fs.readFile(dotGit, "utf8");
289
+ const match = contents.match(/^gitdir:\s*(.+?)\s*$/m);
290
+ if (!match) {
291
+ return null;
292
+ }
293
+ const ref = match[1];
294
+ const resolved = path.isAbsolute(ref) ? ref : path.resolve(dir, ref);
295
+ return { workTree: dir, gitDir: resolved };
296
+ }
297
+ }
298
+ const parent = path.dirname(dir);
299
+ if (parent === dir) break;
300
+ dir = parent;
301
+ }
302
+ return null;
303
+ }
304
+
305
+ // src/git/exclude.ts
306
+ import { promises as fs2 } from "node:fs";
307
+ import path2 from "node:path";
308
+ var HEADER_RE_TEMPLATE = (slug) => new RegExp(
309
+ `^# >>> mcpcall managed \\(workspace: ${escapeRe(slug)}\\) >>>$`
310
+ );
311
+ var FOOTER_RE = /^# <<< mcpcall managed <<<$/;
312
+ function escapeRe(s) {
313
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
314
+ }
315
+ function headerLine(slug) {
316
+ return `# >>> mcpcall managed (workspace: ${slug}) >>>`;
317
+ }
318
+ var FOOTER_LINE = "# <<< mcpcall managed <<<";
319
+ async function readExcludeFile(gitDir) {
320
+ const file = path2.join(gitDir, "info", "exclude");
321
+ try {
322
+ return await fs2.readFile(file, "utf8");
323
+ } catch (err2) {
324
+ const e = err2;
325
+ if (e.code === "ENOENT") {
326
+ await fs2.mkdir(path2.dirname(file), { recursive: true });
327
+ await fs2.writeFile(file, "", "utf8");
328
+ return "";
329
+ }
330
+ throw err2;
331
+ }
332
+ }
333
+ function findBlock(lines, slug) {
334
+ const headerRe = HEADER_RE_TEMPLATE(slug);
335
+ let start = -1;
336
+ for (let i = 0; i < lines.length; i++) {
337
+ if (headerRe.test(lines[i])) {
338
+ start = i;
339
+ break;
340
+ }
341
+ }
342
+ if (start === -1) return null;
343
+ for (let j = start + 1; j < lines.length; j++) {
344
+ if (FOOTER_RE.test(lines[j])) {
345
+ return { start, end: j };
346
+ }
347
+ }
348
+ return null;
349
+ }
350
+ function splitLines(text) {
351
+ if (text === "") return { lines: [], trailingNewline: false };
352
+ const trailingNewline = text.endsWith("\n");
353
+ const body = trailingNewline ? text.slice(0, -1) : text;
354
+ return { lines: body.split("\n"), trailingNewline };
355
+ }
356
+ function joinLines(lines) {
357
+ if (lines.length === 0) return "";
358
+ return lines.join("\n") + "\n";
359
+ }
360
+ async function applyMcpcallBlock(gitDir, workspaceSlug, ignorePaths) {
361
+ const file = path2.join(gitDir, "info", "exclude");
362
+ const current = await readExcludeFile(gitDir);
363
+ const { lines } = splitLines(current);
364
+ const block = [headerLine(workspaceSlug), ...ignorePaths, FOOTER_LINE];
365
+ const existing = findBlock(lines, workspaceSlug);
366
+ let nextLines;
367
+ if (existing) {
368
+ nextLines = [
369
+ ...lines.slice(0, existing.start),
370
+ ...block,
371
+ ...lines.slice(existing.end + 1)
372
+ ];
373
+ } else {
374
+ if (lines.length === 0) {
375
+ nextLines = [...block];
376
+ } else {
377
+ const trimmed = [...lines];
378
+ while (trimmed.length > 0 && trimmed[trimmed.length - 1] === "") trimmed.pop();
379
+ nextLines = [...trimmed, "", ...block];
380
+ }
381
+ }
382
+ const next = joinLines(nextLines);
383
+ if (next !== current) {
384
+ await fs2.writeFile(file, next, "utf8");
385
+ }
386
+ }
387
+ async function removeMcpcallBlock(gitDir, workspaceSlug) {
388
+ const file = path2.join(gitDir, "info", "exclude");
389
+ let current;
390
+ try {
391
+ current = await fs2.readFile(file, "utf8");
392
+ } catch (err2) {
393
+ const e = err2;
394
+ if (e.code === "ENOENT") return false;
395
+ throw err2;
396
+ }
397
+ const { lines } = splitLines(current);
398
+ const existing = findBlock(lines, workspaceSlug);
399
+ if (!existing) return false;
400
+ let cutStart = existing.start;
401
+ if (cutStart > 0 && lines[cutStart - 1] === "") cutStart -= 1;
402
+ const nextLines = [...lines.slice(0, cutStart), ...lines.slice(existing.end + 1)];
403
+ while (nextLines.length > 0 && nextLines[nextLines.length - 1] === "") nextLines.pop();
404
+ const next = joinLines(nextLines);
405
+ await fs2.writeFile(file, next, "utf8");
406
+ return true;
407
+ }
408
+
409
+ // src/manifest.ts
410
+ import { promises as fs3 } from "node:fs";
411
+ import path3 from "node:path";
412
+ import { z as z3 } from "zod";
413
+ var MANIFEST_REL_PATH = path3.join(".claude", ".team-agents.json");
414
+ var localManifestEntrySchema = z3.object({
415
+ /** Workspace-relative path (server-side, e.g. `agents/reviewer.md`). */
416
+ path: z3.string(),
417
+ /** Local on-disk path, project-relative POSIX. */
418
+ projectPath: z3.string(),
419
+ /** Hex SHA-256 of the content we wrote / last synced. */
420
+ sha256: z3.string().length(64)
421
+ });
422
+ var localManifestSchema = z3.object({
423
+ workspaceId: z3.string(),
424
+ workspaceSlug: z3.string(),
425
+ pulledAt: z3.string(),
426
+ apiUrl: z3.string(),
427
+ files: z3.array(localManifestEntrySchema)
428
+ });
429
+ async function readManifest(targetDir) {
430
+ const p = path3.join(targetDir, MANIFEST_REL_PATH);
431
+ try {
432
+ const text = await fs3.readFile(p, "utf8");
433
+ const json = JSON.parse(text);
434
+ return localManifestSchema.parse(json);
435
+ } catch (err2) {
436
+ const e = err2;
437
+ if (e.code === "ENOENT") return null;
438
+ throw err2;
439
+ }
440
+ }
441
+ async function writeManifest(targetDir, manifest) {
442
+ const p = path3.join(targetDir, MANIFEST_REL_PATH);
443
+ await fs3.mkdir(path3.dirname(p), { recursive: true });
444
+ await fs3.writeFile(p, JSON.stringify(manifest, null, 2) + "\n", "utf8");
445
+ }
446
+ async function deleteManifest(targetDir) {
447
+ const p = path3.join(targetDir, MANIFEST_REL_PATH);
448
+ try {
449
+ await fs3.unlink(p);
450
+ return true;
451
+ } catch (err2) {
452
+ const e = err2;
453
+ if (e.code === "ENOENT") return false;
454
+ throw err2;
455
+ }
456
+ }
457
+
458
+ // src/util/sha256.ts
459
+ import { createHash } from "node:crypto";
460
+ function sha256Hex(input) {
461
+ return createHash("sha256").update(input).digest("hex");
462
+ }
463
+
464
+ // src/tools/pullWorkspace.ts
465
+ function resolveDestination(targetDir, workspaceRelPath) {
466
+ const writeRel = projectRelativeWritePath(workspaceRelPath);
467
+ if (writeRel.includes("\\") || writeRel.startsWith("/")) return null;
468
+ const parts = writeRel.split("/");
469
+ if (parts.some((s) => s === ".." || s === "." || s === "")) return null;
470
+ const absTarget = path4.resolve(targetDir);
471
+ const absPath = path4.resolve(absTarget, ...parts);
472
+ const rel = path4.relative(absTarget, absPath);
473
+ if (rel.startsWith("..") || path4.isAbsolute(rel)) return null;
474
+ return { absPath, projectPath: writeRel };
475
+ }
476
+ async function isSymlink(p) {
477
+ try {
478
+ const st = await fs4.lstat(p);
479
+ return st.isSymbolicLink();
480
+ } catch {
481
+ return false;
482
+ }
483
+ }
484
+ async function anyParentIsSymlink(targetDir, absPath) {
485
+ let dir = path4.dirname(absPath);
486
+ const root = path4.resolve(targetDir);
487
+ while (dir.startsWith(root) && dir !== root) {
488
+ if (await isSymlink(dir)) return true;
489
+ const next = path4.dirname(dir);
490
+ if (next === dir) break;
491
+ dir = next;
492
+ }
493
+ return false;
494
+ }
495
+ async function pullWorkspace(api, args) {
496
+ const targetDir = path4.resolve(args.target_dir ?? process.cwd());
497
+ await fs4.mkdir(targetDir, { recursive: true });
498
+ const manifest = await api.listFiles(args.workspace);
499
+ const workspaceSlug = manifest.workspace.slug;
500
+ const workspaceId = manifest.workspace.id;
501
+ const prefixFilter = args.prefix !== void 0 ? args.prefix.replace(/\/+$/, "") : void 0;
502
+ const warnings = [];
503
+ const skipped = [];
504
+ const conflicts = [];
505
+ const writtenEntries = [];
506
+ const successfullyWrittenRelPaths = [];
507
+ for (const entry of manifest.files) {
508
+ if (prefixFilter !== void 0) {
509
+ if (entry.path !== prefixFilter && !entry.path.startsWith(`${prefixFilter}/`)) {
510
+ continue;
511
+ }
512
+ }
513
+ const pathParse = filePathSchema.safeParse(entry.path);
514
+ if (!pathParse.success) {
515
+ skipped.push({ path: entry.path, reason: `invalid path: ${pathParse.error.message}` });
516
+ continue;
517
+ }
518
+ const dest = resolveDestination(targetDir, entry.path);
519
+ if (!dest) {
520
+ skipped.push({ path: entry.path, reason: "path resolves outside target dir" });
521
+ continue;
522
+ }
523
+ if (await anyParentIsSymlink(targetDir, dest.absPath)) {
524
+ skipped.push({ path: entry.path, reason: "symlinked parent directory" });
525
+ continue;
526
+ }
527
+ if (await isSymlink(dest.absPath)) {
528
+ skipped.push({ path: entry.path, reason: "destination is a symlink" });
529
+ continue;
530
+ }
531
+ let remoteContent;
532
+ try {
533
+ remoteContent = await api.getFileContent(workspaceSlug, entry.path);
534
+ } catch (err2) {
535
+ skipped.push({ path: entry.path, reason: `fetch failed: ${err2.message}` });
536
+ continue;
537
+ }
538
+ const remoteHash = sha256Hex(remoteContent);
539
+ let existing = null;
540
+ try {
541
+ existing = await fs4.readFile(dest.absPath, "utf8");
542
+ } catch (err2) {
543
+ const e = err2;
544
+ if (e.code !== "ENOENT") {
545
+ skipped.push({ path: entry.path, reason: `read failed: ${e.message}` });
546
+ continue;
547
+ }
548
+ }
549
+ if (existing !== null) {
550
+ const existingHash = sha256Hex(existing);
551
+ if (existingHash === remoteHash) {
552
+ writtenEntries.push({
553
+ path: entry.path,
554
+ projectPath: dest.projectPath,
555
+ sha256: remoteHash
556
+ });
557
+ successfullyWrittenRelPaths.push(entry.path);
558
+ continue;
559
+ }
560
+ const incomingAbs = `${dest.absPath}.incoming`;
561
+ await fs4.mkdir(path4.dirname(incomingAbs), { recursive: true });
562
+ await fs4.writeFile(incomingAbs, remoteContent, "utf8");
563
+ conflicts.push({
564
+ path: entry.path,
565
+ incomingPath: `${dest.projectPath}.incoming`
566
+ });
567
+ successfullyWrittenRelPaths.push(entry.path);
568
+ writtenEntries.push({
569
+ path: entry.path,
570
+ projectPath: dest.projectPath,
571
+ sha256: existingHash
572
+ });
573
+ continue;
574
+ }
575
+ await fs4.mkdir(path4.dirname(dest.absPath), { recursive: true });
576
+ await fs4.writeFile(dest.absPath, remoteContent, "utf8");
577
+ writtenEntries.push({
578
+ path: entry.path,
579
+ projectPath: dest.projectPath,
580
+ sha256: remoteHash
581
+ });
582
+ successfullyWrittenRelPaths.push(entry.path);
583
+ }
584
+ const gitInfo = await findGitDir(targetDir);
585
+ let excludeUpdated = false;
586
+ if (!gitInfo) {
587
+ warnings.push(
588
+ `No git repository detected at or above ${targetDir}. Files were written but NOT hidden from any VCS \u2014 add them to your ignore manually.`
589
+ );
590
+ } else {
591
+ const ignorePaths = topLevelExcludePaths(successfullyWrittenRelPaths);
592
+ if (ignorePaths.length > 0) {
593
+ await applyMcpcallBlock(gitInfo.gitDir, workspaceSlug, ignorePaths);
594
+ excludeUpdated = true;
595
+ }
596
+ }
597
+ const local = {
598
+ workspaceId,
599
+ workspaceSlug,
600
+ pulledAt: (/* @__PURE__ */ new Date()).toISOString(),
601
+ apiUrl: api.apiUrl,
602
+ files: writtenEntries
603
+ };
604
+ await writeManifest(targetDir, local);
605
+ return {
606
+ workspaceId,
607
+ workspaceSlug,
608
+ targetDir,
609
+ written: writtenEntries.length,
610
+ skipped,
611
+ conflicts,
612
+ excludeUpdated,
613
+ gitDetected: gitInfo !== null,
614
+ warnings
615
+ };
616
+ }
617
+
618
+ // src/tools/pullFile.ts
619
+ import { promises as fs5 } from "node:fs";
620
+ import path5 from "node:path";
621
+ async function isSymlink2(p) {
622
+ try {
623
+ const st = await fs5.lstat(p);
624
+ return st.isSymbolicLink();
625
+ } catch {
626
+ return false;
627
+ }
628
+ }
629
+ async function anyParentIsSymlink2(targetDir, absPath) {
630
+ let dir = path5.dirname(absPath);
631
+ const root = path5.resolve(targetDir);
632
+ while (dir.startsWith(root) && dir !== root) {
633
+ if (await isSymlink2(dir)) return true;
634
+ const next = path5.dirname(dir);
635
+ if (next === dir) break;
636
+ dir = next;
637
+ }
638
+ return false;
639
+ }
640
+ async function pullFile(api, args) {
641
+ const targetDir = path5.resolve(args.target_dir ?? process.cwd());
642
+ await fs5.mkdir(targetDir, { recursive: true });
643
+ const filePath = filePathSchema.parse(args.path);
644
+ const warnings = [];
645
+ const wsManifest = await api.listFiles(args.workspace);
646
+ const workspaceSlug = wsManifest.workspace.slug;
647
+ const workspaceId = wsManifest.workspace.id;
648
+ const remoteEntry = wsManifest.files.find((f) => f.path === filePath);
649
+ if (!remoteEntry) {
650
+ throw new Error(`File '${filePath}' not found in workspace '${workspaceSlug}'.`);
651
+ }
652
+ const writeRel = projectRelativeWritePath(filePath);
653
+ const parts = writeRel.split("/");
654
+ const absPath = path5.resolve(targetDir, ...parts);
655
+ const rel = path5.relative(path5.resolve(targetDir), absPath);
656
+ if (rel.startsWith("..") || path5.isAbsolute(rel)) {
657
+ throw new Error(`Resolved destination ${absPath} would escape target directory.`);
658
+ }
659
+ if (await anyParentIsSymlink2(targetDir, absPath)) {
660
+ throw new Error(`A parent directory of the destination is a symlink \u2014 refusing to write.`);
661
+ }
662
+ if (await isSymlink2(absPath)) {
663
+ throw new Error(`Destination ${writeRel} is a symlink \u2014 refusing to overwrite.`);
664
+ }
665
+ const remoteContent = await api.getFileContent(workspaceSlug, filePath);
666
+ const remoteHash = sha256Hex(remoteContent);
667
+ let existing = null;
668
+ try {
669
+ existing = await fs5.readFile(absPath, "utf8");
670
+ } catch (err2) {
671
+ const e = err2;
672
+ if (e.code !== "ENOENT") {
673
+ throw new Error(`Cannot read existing file ${writeRel}: ${e.message}`);
674
+ }
675
+ }
676
+ let written = false;
677
+ let conflict = false;
678
+ let incomingPath = null;
679
+ let recordedHash;
680
+ if (existing !== null) {
681
+ const existingHash = sha256Hex(existing);
682
+ if (existingHash === remoteHash) {
683
+ written = false;
684
+ recordedHash = remoteHash;
685
+ } else {
686
+ const incomingAbs = `${absPath}.incoming`;
687
+ await fs5.mkdir(path5.dirname(incomingAbs), { recursive: true });
688
+ await fs5.writeFile(incomingAbs, remoteContent, "utf8");
689
+ conflict = true;
690
+ incomingPath = `${writeRel}.incoming`;
691
+ recordedHash = existingHash;
692
+ }
693
+ } else {
694
+ await fs5.mkdir(path5.dirname(absPath), { recursive: true });
695
+ await fs5.writeFile(absPath, remoteContent, "utf8");
696
+ written = true;
697
+ recordedHash = remoteHash;
698
+ }
699
+ const existingManifest = await readManifest(targetDir);
700
+ const newEntry = {
701
+ path: filePath,
702
+ projectPath: writeRel,
703
+ sha256: recordedHash
704
+ };
705
+ let manifest;
706
+ if (existingManifest) {
707
+ const idx = existingManifest.files.findIndex((f) => f.path === filePath);
708
+ if (idx >= 0) {
709
+ existingManifest.files[idx] = newEntry;
710
+ } else {
711
+ existingManifest.files.push(newEntry);
712
+ }
713
+ manifest = { ...existingManifest, pulledAt: (/* @__PURE__ */ new Date()).toISOString() };
714
+ } else {
715
+ manifest = {
716
+ workspaceId,
717
+ workspaceSlug,
718
+ pulledAt: (/* @__PURE__ */ new Date()).toISOString(),
719
+ apiUrl: api.apiUrl,
720
+ files: [newEntry]
721
+ };
722
+ }
723
+ await writeManifest(targetDir, manifest);
724
+ const gitInfo = await findGitDir(targetDir);
725
+ let excludeUpdated = false;
726
+ if (!gitInfo) {
727
+ warnings.push(
728
+ `No git repository detected at or above ${targetDir}. File was written but NOT hidden from any VCS \u2014 add it to your ignore manually.`
729
+ );
730
+ } else {
731
+ const allProjectPaths = manifest.files.map((f) => f.path);
732
+ const ignorePaths = topLevelExcludePaths(allProjectPaths);
733
+ if (ignorePaths.length > 0) {
734
+ await applyMcpcallBlock(gitInfo.gitDir, workspaceSlug, ignorePaths);
735
+ excludeUpdated = true;
736
+ }
737
+ }
738
+ return {
739
+ workspaceSlug,
740
+ path: filePath,
741
+ projectPath: writeRel,
742
+ sha256: recordedHash,
743
+ written,
744
+ conflict,
745
+ incomingPath,
746
+ excludeUpdated,
747
+ gitDetected: gitInfo !== null,
748
+ warnings
749
+ };
750
+ }
751
+
752
+ // src/tools/importFolder.ts
753
+ import { promises as fs6 } from "node:fs";
754
+ import path6 from "node:path";
755
+ var MAX_FILES = 200;
756
+ var MAX_BYTES = 2 * 1024 * 1024;
757
+ function matchGlob(pattern, filePath) {
758
+ const p = filePath.replace(/\\/g, "/");
759
+ const g = pattern.replace(/\\/g, "/");
760
+ let re = "";
761
+ let i = 0;
762
+ while (i < g.length) {
763
+ const ch = g[i];
764
+ if (ch === "*" && g[i + 1] === "*") {
765
+ re += ".*";
766
+ i += 2;
767
+ if (g[i] === "/") i += 1;
768
+ } else if (ch === "*") {
769
+ re += "[^/]*";
770
+ i += 1;
771
+ } else if (ch === "?") {
772
+ re += "[^/]";
773
+ i += 1;
774
+ } else if (/[.+^${}()|[\]\\]/.test(ch)) {
775
+ re += "\\" + ch;
776
+ i += 1;
777
+ } else {
778
+ re += ch;
779
+ i += 1;
780
+ }
781
+ }
782
+ return new RegExp(`^${re}$`).test(p);
783
+ }
784
+ function matchesAnyGlob(patterns, filePath) {
785
+ return patterns.some((pat) => matchGlob(pat, filePath));
786
+ }
787
+ async function walkDir(sourceDir, relBase, collector) {
788
+ let entries;
789
+ try {
790
+ entries = await fs6.readdir(path6.join(sourceDir, relBase), { withFileTypes: true });
791
+ } catch (err2) {
792
+ const e = err2;
793
+ throw new Error(`Cannot read directory ${path6.join(sourceDir, relBase)}: ${e.message}`);
794
+ }
795
+ for (const entry of entries) {
796
+ const entryRel = relBase ? `${relBase}/${entry.name}` : entry.name;
797
+ const entryAbs = path6.join(sourceDir, entryRel);
798
+ const st = await fs6.lstat(entryAbs);
799
+ if (st.isSymbolicLink()) {
800
+ collector.push({ rel: `__symlink__:${entryRel}`, abs: entryAbs });
801
+ continue;
802
+ }
803
+ if (entry.isDirectory()) {
804
+ await walkDir(sourceDir, entryRel, collector);
805
+ } else if (entry.isFile()) {
806
+ collector.push({ rel: entryRel, abs: entryAbs });
807
+ }
808
+ }
809
+ }
810
+ function validateDestPrefix(raw) {
811
+ const p = raw.replace(/\\/g, "/").replace(/\/+$/, "");
812
+ if (p === "") return "";
813
+ filePathSchema.parse(p);
814
+ return p;
815
+ }
816
+ async function importFolder(api, args) {
817
+ const dryRun = args.dry_run === true;
818
+ const include = args.include ?? ["**/*.md", "**/*.mdx"];
819
+ const destPrefix = args.dest_prefix !== void 0 ? validateDestPrefix(args.dest_prefix) : "";
820
+ const sourceDir = path6.resolve(args.source_dir);
821
+ let stat;
822
+ try {
823
+ stat = await fs6.stat(sourceDir);
824
+ } catch (err2) {
825
+ const e = err2;
826
+ throw new Error(`source_dir does not exist or is not accessible: ${e.message}`);
827
+ }
828
+ if (!stat.isDirectory()) {
829
+ throw new Error(`source_dir is not a directory: ${sourceDir}`);
830
+ }
831
+ const allEntries = [];
832
+ await walkDir(sourceDir, "", allEntries);
833
+ const uploaded = [];
834
+ const skipped = [];
835
+ const realEntries = allEntries.filter((e) => !e.rel.startsWith("__symlink__:"));
836
+ const symlinkEntries = allEntries.filter((e) => e.rel.startsWith("__symlink__:"));
837
+ for (const s of symlinkEntries) {
838
+ skipped.push({ path: s.rel.slice("__symlink__:".length), reason: "symlink \u2014 skipped" });
839
+ }
840
+ const matched = realEntries.filter((e) => matchesAnyGlob(include, e.rel));
841
+ if (matched.length > MAX_FILES) {
842
+ throw new Error(
843
+ `Found ${matched.length} files matching the include patterns \u2014 exceeds the cap of ${MAX_FILES}. Use a narrower 'include' glob to proceed.`
844
+ );
845
+ }
846
+ for (const entry of matched) {
847
+ const wsPath = destPrefix ? `${destPrefix}/${entry.rel}` : entry.rel;
848
+ const pathParse = filePathSchema.safeParse(wsPath);
849
+ if (!pathParse.success) {
850
+ skipped.push({ path: wsPath, reason: `invalid workspace path: ${pathParse.error.message}` });
851
+ continue;
852
+ }
853
+ let fileStat;
854
+ try {
855
+ fileStat = await fs6.stat(entry.abs);
856
+ } catch (err2) {
857
+ skipped.push({ path: wsPath, reason: `stat failed: ${err2.message}` });
858
+ continue;
859
+ }
860
+ if (fileStat.size > MAX_BYTES) {
861
+ skipped.push({ path: wsPath, reason: `file too large (${fileStat.size} bytes > 2 MB limit)` });
862
+ continue;
863
+ }
864
+ if (dryRun) {
865
+ uploaded.push({ path: wsPath, sizeBytes: fileStat.size });
866
+ continue;
867
+ }
868
+ let content;
869
+ try {
870
+ content = await fs6.readFile(entry.abs, "utf8");
871
+ } catch (err2) {
872
+ skipped.push({ path: wsPath, reason: `read failed: ${err2.message}` });
873
+ continue;
874
+ }
875
+ const sizeBytes = Buffer.byteLength(content, "utf8");
876
+ try {
877
+ await api.putFile(args.workspace, wsPath, content);
878
+ uploaded.push({ path: wsPath, sizeBytes });
879
+ } catch (err2) {
880
+ skipped.push({ path: wsPath, reason: `upload failed: ${err2.message}` });
881
+ }
882
+ }
883
+ return {
884
+ workspaceSlug: args.workspace,
885
+ sourceDir,
886
+ uploaded,
887
+ skipped,
888
+ dryRun
889
+ };
890
+ }
891
+
892
+ // src/tools/createFile.ts
893
+ async function createFile(api, args) {
894
+ const filePath = filePathSchema.parse(args.path);
895
+ const kind = fileKindSchema.parse(args.kind);
896
+ await api.putFile(args.workspace, filePath, args.content, kind);
897
+ if (args.target_dir) {
898
+ const manifest = await readManifest(args.target_dir);
899
+ if (manifest && manifest.workspaceSlug === args.workspace) {
900
+ const sha = sha256Hex(args.content);
901
+ const projectPath = projectRelativeWritePath(filePath);
902
+ const idx = manifest.files.findIndex((f) => f.path === filePath);
903
+ const entry = { path: filePath, projectPath, sha256: sha };
904
+ if (idx >= 0) manifest.files[idx] = entry;
905
+ else manifest.files.push(entry);
906
+ await writeManifest(args.target_dir, manifest);
907
+ }
908
+ }
909
+ return { workspace: args.workspace, path: filePath, kind, sha256: sha256Hex(args.content) };
910
+ }
911
+
912
+ // src/tools/pushFile.ts
913
+ import { promises as fs7 } from "node:fs";
914
+ import path7 from "node:path";
915
+ async function pushFile(api, args) {
916
+ const filePath = filePathSchema.parse(args.path);
917
+ const targetDir = path7.resolve(args.target_dir ?? process.cwd());
918
+ let content = args.content;
919
+ if (content === void 0) {
920
+ const projectPath = projectRelativeWritePath(filePath);
921
+ const abs = path7.resolve(targetDir, projectPath);
922
+ try {
923
+ content = await fs7.readFile(abs, "utf8");
924
+ } catch (err2) {
925
+ const e = err2;
926
+ throw new Error(`Cannot read local file ${projectPath}: ${e.message}`);
927
+ }
928
+ }
929
+ await api.putFile(args.workspace, filePath, content);
930
+ const sha = sha256Hex(content);
931
+ const manifest = await readManifest(targetDir);
932
+ if (manifest && manifest.workspaceSlug === args.workspace) {
933
+ const projectPath = projectRelativeWritePath(filePath);
934
+ const entry = { path: filePath, projectPath, sha256: sha };
935
+ const idx = manifest.files.findIndex((f) => f.path === filePath);
936
+ if (idx >= 0) manifest.files[idx] = entry;
937
+ else manifest.files.push(entry);
938
+ await writeManifest(targetDir, manifest);
939
+ }
940
+ return { workspace: args.workspace, path: filePath, sha256: sha };
941
+ }
942
+
943
+ // src/tools/deleteFile.ts
944
+ import path8 from "node:path";
945
+ async function deleteFile(api, args) {
946
+ const filePath = filePathSchema.parse(args.path);
947
+ await api.deleteFile(args.workspace, filePath);
948
+ const targetDir = path8.resolve(args.target_dir ?? process.cwd());
949
+ const manifest = await readManifest(targetDir);
950
+ if (manifest && manifest.workspaceSlug === args.workspace) {
951
+ const before = manifest.files.length;
952
+ manifest.files = manifest.files.filter((f) => f.path !== filePath);
953
+ if (manifest.files.length !== before) {
954
+ await writeManifest(targetDir, manifest);
955
+ }
956
+ }
957
+ return { workspace: args.workspace, path: filePath, deleted: true };
958
+ }
959
+
960
+ // src/tools/cleanup.ts
961
+ import { promises as fs8 } from "node:fs";
962
+ import path9 from "node:path";
963
+ async function pruneEmptyParents(absFile, stopAt) {
964
+ let dir = path9.dirname(absFile);
965
+ const root = path9.resolve(stopAt);
966
+ while (dir.startsWith(root) && dir !== root) {
967
+ try {
968
+ const entries = await fs8.readdir(dir);
969
+ if (entries.length > 0) return;
970
+ await fs8.rmdir(dir);
971
+ } catch {
972
+ return;
973
+ }
974
+ const next = path9.dirname(dir);
975
+ if (next === dir) break;
976
+ dir = next;
977
+ }
978
+ }
979
+ async function cleanup(args) {
980
+ const targetDir = path9.resolve(args.target_dir ?? process.cwd());
981
+ const manifest = await readManifest(targetDir);
982
+ if (!manifest) {
983
+ return {
984
+ removed: 0,
985
+ skipped: [],
986
+ excludeBlockRemoved: false,
987
+ manifestRemoved: false,
988
+ workspaceSlug: null
989
+ };
990
+ }
991
+ const force = args.force === true;
992
+ let removed = 0;
993
+ const skipped = [];
994
+ for (const entry of manifest.files) {
995
+ const abs = path9.resolve(targetDir, ...entry.projectPath.split("/"));
996
+ let content;
997
+ try {
998
+ content = await fs8.readFile(abs, "utf8");
999
+ } catch (err2) {
1000
+ const e = err2;
1001
+ if (e.code === "ENOENT") {
1002
+ removed += 1;
1003
+ continue;
1004
+ }
1005
+ skipped.push({ path: entry.path, reason: `read failed: ${e.message}` });
1006
+ continue;
1007
+ }
1008
+ const localHash = sha256Hex(content);
1009
+ if (!force && localHash !== entry.sha256) {
1010
+ skipped.push({
1011
+ path: entry.path,
1012
+ reason: "local edits detected (sha mismatch). Re-run with force: true to override."
1013
+ });
1014
+ continue;
1015
+ }
1016
+ try {
1017
+ await fs8.unlink(abs);
1018
+ removed += 1;
1019
+ await pruneEmptyParents(abs, targetDir);
1020
+ } catch (err2) {
1021
+ const e = err2;
1022
+ skipped.push({ path: entry.path, reason: `unlink failed: ${e.message}` });
1023
+ }
1024
+ }
1025
+ let excludeBlockRemoved = false;
1026
+ const gitInfo = await findGitDir(targetDir);
1027
+ if (gitInfo) {
1028
+ excludeBlockRemoved = await removeMcpcallBlock(gitInfo.gitDir, manifest.workspaceSlug);
1029
+ }
1030
+ const manifestRemoved = await deleteManifest(targetDir);
1031
+ const claudeDir = path9.join(targetDir, ".claude");
1032
+ try {
1033
+ const remaining = await fs8.readdir(claudeDir);
1034
+ if (remaining.length === 0) await fs8.rmdir(claudeDir);
1035
+ } catch {
1036
+ }
1037
+ return {
1038
+ removed,
1039
+ skipped,
1040
+ excludeBlockRemoved,
1041
+ manifestRemoved,
1042
+ workspaceSlug: manifest.workspaceSlug
1043
+ };
1044
+ }
1045
+
1046
+ // src/tools/status.ts
1047
+ import { promises as fs9 } from "node:fs";
1048
+ import path10 from "node:path";
1049
+ async function status(api, args) {
1050
+ const targetDir = path10.resolve(args.target_dir ?? process.cwd());
1051
+ const manifest = await readManifest(targetDir);
1052
+ if (!manifest) {
1053
+ return {
1054
+ workspaceId: null,
1055
+ workspaceSlug: null,
1056
+ pulledAt: null,
1057
+ targetDir,
1058
+ files: [],
1059
+ remoteOnly: [],
1060
+ hasManifest: false
1061
+ };
1062
+ }
1063
+ const remote = await api.listFiles(manifest.workspaceSlug);
1064
+ const remoteBy = new Map(remote.files.map((f) => [f.path, f]));
1065
+ const seenRemote = /* @__PURE__ */ new Set();
1066
+ const files = [];
1067
+ for (const entry of manifest.files) {
1068
+ const remoteEntry = remoteBy.get(entry.path);
1069
+ const absLocal = path10.resolve(targetDir, ...entry.projectPath.split("/"));
1070
+ let localSha = null;
1071
+ try {
1072
+ const content = await fs9.readFile(absLocal, "utf8");
1073
+ localSha = sha256Hex(content);
1074
+ } catch (err2) {
1075
+ const e = err2;
1076
+ if (e.code !== "ENOENT") throw err2;
1077
+ }
1078
+ if (remoteEntry) seenRemote.add(entry.path);
1079
+ let s;
1080
+ if (!remoteEntry) {
1081
+ s = "remote_missing";
1082
+ } else if (localSha === null) {
1083
+ s = "local_missing";
1084
+ } else {
1085
+ const local = localSha === entry.sha256;
1086
+ const remoteSame = remoteEntry.sha256 === entry.sha256;
1087
+ if (local && remoteSame) s = "in_sync";
1088
+ else if (!local && remoteSame) s = "local_drift";
1089
+ else if (local && !remoteSame) s = "remote_drift";
1090
+ else s = "conflict";
1091
+ }
1092
+ files.push({
1093
+ path: entry.path,
1094
+ status: s,
1095
+ localSha,
1096
+ remoteSha: remoteEntry?.sha256 ?? null,
1097
+ manifestSha: entry.sha256
1098
+ });
1099
+ }
1100
+ const remoteOnly = remote.files.filter((f) => !seenRemote.has(f.path) && !manifest.files.some((m) => m.path === f.path)).map((f) => ({ path: f.path, sha256: f.sha256 }));
1101
+ return {
1102
+ workspaceId: manifest.workspaceId,
1103
+ workspaceSlug: manifest.workspaceSlug,
1104
+ pulledAt: manifest.pulledAt,
1105
+ targetDir,
1106
+ files,
1107
+ remoteOnly,
1108
+ hasManifest: true
1109
+ };
1110
+ }
1111
+
1112
+ // src/index.ts
1113
+ var listWorkspacesInput = z4.object({}).strict();
1114
+ var listFilesInput = z4.object({ workspace: workspaceSlugSchema }).strict();
1115
+ var pullWorkspaceInput = z4.object({
1116
+ workspace: workspaceSlugSchema,
1117
+ target_dir: z4.string().min(1).optional(),
1118
+ prefix: z4.string().min(1).optional()
1119
+ }).strict();
1120
+ var pullFileInput = z4.object({
1121
+ workspace: workspaceSlugSchema,
1122
+ path: filePathSchema,
1123
+ target_dir: z4.string().min(1).optional()
1124
+ }).strict();
1125
+ var importFolderInput = z4.object({
1126
+ workspace: workspaceSlugSchema,
1127
+ source_dir: z4.string().min(1),
1128
+ include: z4.array(z4.string().min(1)).optional(),
1129
+ dest_prefix: z4.string().optional(),
1130
+ dry_run: z4.boolean().optional()
1131
+ }).strict();
1132
+ var createFileInput = z4.object({
1133
+ workspace: workspaceSlugSchema,
1134
+ path: filePathSchema,
1135
+ kind: fileKindSchema,
1136
+ content: z4.string(),
1137
+ target_dir: z4.string().min(1).optional()
1138
+ }).strict();
1139
+ var pushFileInput = z4.object({
1140
+ workspace: workspaceSlugSchema,
1141
+ path: filePathSchema,
1142
+ content: z4.string().optional(),
1143
+ target_dir: z4.string().min(1).optional()
1144
+ }).strict();
1145
+ var deleteFileInput = z4.object({
1146
+ workspace: workspaceSlugSchema,
1147
+ path: filePathSchema,
1148
+ target_dir: z4.string().min(1).optional()
1149
+ }).strict();
1150
+ var cleanupInput = z4.object({
1151
+ target_dir: z4.string().min(1).optional(),
1152
+ force: z4.boolean().optional()
1153
+ }).strict();
1154
+ var statusInput = z4.object({ target_dir: z4.string().min(1).optional() }).strict();
1155
+ var TOOLS = [
1156
+ {
1157
+ name: "list_workspaces",
1158
+ description: "List workspaces accessible to the configured API key.",
1159
+ inputSchema: { type: "object", properties: {}, additionalProperties: false }
1160
+ },
1161
+ {
1162
+ name: "list_files",
1163
+ description: "List files in a workspace with size, sha256, and last-modified timestamps.",
1164
+ inputSchema: {
1165
+ type: "object",
1166
+ properties: { workspace: { type: "string", description: "Workspace slug." } },
1167
+ required: ["workspace"],
1168
+ additionalProperties: false
1169
+ }
1170
+ },
1171
+ {
1172
+ name: "pull_workspace",
1173
+ description: "Pull all files (or a filtered subset) for a workspace into the target directory's `.claude/` (and root for CLAUDE.md/RULES.md), and append a sentinel block to `.git/info/exclude` so the client's git status stays clean. Never clobbers local edits \u2014 writes `.incoming` siblings on conflict. Use `prefix` to pull only files whose workspace path starts with that prefix (e.g. `agents` to pull the agents subtree).",
1174
+ inputSchema: {
1175
+ type: "object",
1176
+ properties: {
1177
+ workspace: { type: "string", description: "Workspace slug." },
1178
+ target_dir: {
1179
+ type: "string",
1180
+ description: "Absolute target directory. Defaults to process.cwd()."
1181
+ },
1182
+ prefix: {
1183
+ type: "string",
1184
+ description: "Only pull files whose workspace path starts with this prefix or equals it exactly. E.g. 'agents' pulls only the agents subtree; 'agents/reviewer.md' pulls a single file."
1185
+ }
1186
+ },
1187
+ required: ["workspace"],
1188
+ additionalProperties: false
1189
+ }
1190
+ },
1191
+ {
1192
+ name: "pull_file",
1193
+ description: "Pull a single file from a workspace into the project's `.claude/` directory (or project root for CLAUDE.md/RULES.md). Applies the same conflict-safe semantics as pull_workspace: writes a `.incoming` sibling on conflict, updates the local manifest, and refreshes the `.git/info/exclude` sentinel block.",
1194
+ inputSchema: {
1195
+ type: "object",
1196
+ properties: {
1197
+ workspace: { type: "string", description: "Workspace slug." },
1198
+ path: {
1199
+ type: "string",
1200
+ description: "Workspace-relative file path (POSIX), e.g. `agents/reviewer.md`."
1201
+ },
1202
+ target_dir: {
1203
+ type: "string",
1204
+ description: "Absolute target directory. Defaults to process.cwd()."
1205
+ }
1206
+ },
1207
+ required: ["workspace", "path"],
1208
+ additionalProperties: false
1209
+ }
1210
+ },
1211
+ {
1212
+ name: "import_folder",
1213
+ description: "Bulk-import a local directory tree into a workspace. Walks the source directory, filters by glob patterns, and uploads matched files via PUT. Validates each workspace path, rejects symlinks, skips files > 2 MB, and caps at 200 files per call. Use dry_run: true to preview what would be uploaded without making any changes.",
1214
+ inputSchema: {
1215
+ type: "object",
1216
+ properties: {
1217
+ workspace: { type: "string", description: "Workspace slug." },
1218
+ source_dir: {
1219
+ type: "string",
1220
+ description: "Absolute or cwd-relative path to the local directory to import from."
1221
+ },
1222
+ include: {
1223
+ type: "array",
1224
+ items: { type: "string" },
1225
+ description: "Minimatch-style glob patterns of files to include. Defaults to ['**/*.md', '**/*.mdx']. Supports `**` (recursive), `*` (single segment), and exact paths."
1226
+ },
1227
+ dest_prefix: {
1228
+ type: "string",
1229
+ description: "Prepend to every uploaded file's workspace path. E.g. dest_prefix='agents/' makes './reviewer.md' upload as 'agents/reviewer.md'."
1230
+ },
1231
+ dry_run: {
1232
+ type: "boolean",
1233
+ description: "List what would be uploaded without uploading. Defaults to false."
1234
+ }
1235
+ },
1236
+ required: ["workspace", "source_dir"],
1237
+ additionalProperties: false
1238
+ }
1239
+ },
1240
+ {
1241
+ name: "create_file",
1242
+ description: "Create a new file on the platform.",
1243
+ inputSchema: {
1244
+ type: "object",
1245
+ properties: {
1246
+ workspace: { type: "string" },
1247
+ path: { type: "string", description: "Workspace-relative path (POSIX)." },
1248
+ kind: {
1249
+ type: "string",
1250
+ enum: ["claude_md", "agent", "skill", "command", "rule", "other"]
1251
+ },
1252
+ content: { type: "string" },
1253
+ target_dir: { type: "string" }
1254
+ },
1255
+ required: ["workspace", "path", "kind", "content"],
1256
+ additionalProperties: false
1257
+ }
1258
+ },
1259
+ {
1260
+ name: "push_file",
1261
+ description: "Upsert a file on the platform. If `content` is omitted, reads the file from the local on-disk path resolved via projectRelativeWritePath.",
1262
+ inputSchema: {
1263
+ type: "object",
1264
+ properties: {
1265
+ workspace: { type: "string" },
1266
+ path: { type: "string" },
1267
+ content: { type: "string" },
1268
+ target_dir: { type: "string" }
1269
+ },
1270
+ required: ["workspace", "path"],
1271
+ additionalProperties: false
1272
+ }
1273
+ },
1274
+ {
1275
+ name: "delete_file",
1276
+ description: "Soft-delete a file on the platform. Does not touch local disk.",
1277
+ inputSchema: {
1278
+ type: "object",
1279
+ properties: {
1280
+ workspace: { type: "string" },
1281
+ path: { type: "string" },
1282
+ target_dir: { type: "string" }
1283
+ },
1284
+ required: ["workspace", "path"],
1285
+ additionalProperties: false
1286
+ }
1287
+ },
1288
+ {
1289
+ name: "cleanup",
1290
+ description: "Reverse a previous pull: delete files whose local sha matches what was written, strip the sentinel block from `.git/info/exclude`, and remove the local manifest. Files with local edits are skipped unless `force` is true.",
1291
+ inputSchema: {
1292
+ type: "object",
1293
+ properties: {
1294
+ target_dir: { type: "string" },
1295
+ force: { type: "boolean" }
1296
+ },
1297
+ additionalProperties: false
1298
+ }
1299
+ },
1300
+ {
1301
+ name: "status",
1302
+ description: "Compare local manifest against remote manifest, reporting drift per file.",
1303
+ inputSchema: {
1304
+ type: "object",
1305
+ properties: { target_dir: { type: "string" } },
1306
+ additionalProperties: false
1307
+ }
1308
+ }
1309
+ ];
1310
+ function ok(value) {
1311
+ return {
1312
+ content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
1313
+ };
1314
+ }
1315
+ function err(message) {
1316
+ return {
1317
+ isError: true,
1318
+ content: [{ type: "text", text: message }]
1319
+ };
1320
+ }
1321
+ function formatError(e) {
1322
+ if (e instanceof ApiError) {
1323
+ return `API error (${e.status} ${e.code}): ${e.message}`;
1324
+ }
1325
+ if (e instanceof z4.ZodError) {
1326
+ return `Invalid arguments: ${e.errors.map((x) => `${x.path.join(".") || "<root>"}: ${x.message}`).join("; ")}`;
1327
+ }
1328
+ if (e instanceof Error) return e.message;
1329
+ return String(e);
1330
+ }
1331
+ async function main() {
1332
+ const server = new Server(
1333
+ { name: "mcpcall", version: "0.1.0" },
1334
+ { capabilities: { tools: {} } }
1335
+ );
1336
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
1337
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1338
+ const { name, arguments: rawArgs } = req.params;
1339
+ const args = rawArgs ?? {};
1340
+ try {
1341
+ const api = () => new ApiClient();
1342
+ switch (name) {
1343
+ case "list_workspaces": {
1344
+ listWorkspacesInput.parse(args);
1345
+ return ok(await listWorkspaces(api()));
1346
+ }
1347
+ case "list_files": {
1348
+ const parsed = listFilesInput.parse(args);
1349
+ return ok(await listFiles(api(), parsed.workspace));
1350
+ }
1351
+ case "pull_workspace": {
1352
+ const parsed = pullWorkspaceInput.parse(args);
1353
+ return ok(await pullWorkspace(api(), parsed));
1354
+ }
1355
+ case "pull_file": {
1356
+ const parsed = pullFileInput.parse(args);
1357
+ return ok(await pullFile(api(), parsed));
1358
+ }
1359
+ case "import_folder": {
1360
+ const parsed = importFolderInput.parse(args);
1361
+ return ok(await importFolder(api(), parsed));
1362
+ }
1363
+ case "create_file": {
1364
+ const parsed = createFileInput.parse(args);
1365
+ return ok(await createFile(api(), parsed));
1366
+ }
1367
+ case "push_file": {
1368
+ const parsed = pushFileInput.parse(args);
1369
+ return ok(await pushFile(api(), parsed));
1370
+ }
1371
+ case "delete_file": {
1372
+ const parsed = deleteFileInput.parse(args);
1373
+ return ok(await deleteFile(api(), parsed));
1374
+ }
1375
+ case "cleanup": {
1376
+ const parsed = cleanupInput.parse(args);
1377
+ return ok(await cleanup(parsed));
1378
+ }
1379
+ case "status": {
1380
+ const parsed = statusInput.parse(args);
1381
+ return ok(await status(api(), parsed));
1382
+ }
1383
+ default:
1384
+ return err(`Unknown tool: ${name}`);
1385
+ }
1386
+ } catch (e) {
1387
+ return err(formatError(e));
1388
+ }
1389
+ });
1390
+ const transport = new StdioServerTransport();
1391
+ await server.connect(transport);
1392
+ }
1393
+ main().catch((e) => {
1394
+ console.error("[mcpcall] fatal:", e);
1395
+ process.exit(1);
1396
+ });