@shippp/mcp 0.1.0 → 0.2.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shippp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @shippp/mcp
2
2
 
3
- Pull production-perfect Chakra UI code from your Shippp design files straight into your IDE agent (Claude Code, Cursor, …).
3
+ Pull production-perfect UI code from your Shippp design files straight into your IDE agent (Claude Code, Cursor, …), and keep a repository current over the official receipt-tracked sync.
4
4
 
5
5
  ## Setup
6
6
 
@@ -34,5 +34,37 @@ Pull production-perfect Chakra UI code from your Shippp design files straight in
34
34
  | `shippp_get_design_system` | The whole design system as a package. |
35
35
  | `shippp_get_project` | The whole file as a runnable Next.js project. |
36
36
  | `shippp_diff` | Your current code vs a fresh generation. |
37
+ | `shippp_sync_plan` | Deterministic sync plan for the bound repository — no writes. |
38
+ | `shippp_sync_apply` | The full sync run: artifact → plan → apply → checks → receipt. |
37
39
 
38
- Read-only. The server never modifies your design.
40
+ The server never modifies your design. The sync tools write ONLY under the
41
+ generated root recorded in the repo's own committed `shippp.target.json`
42
+ binding (created with `shippp_bind_repository` on the Shippp remote MCP):
43
+ hand-modified generated files are structured conflicts and are never
44
+ overwritten, a failed check rolls everything back, and the receipt is written
45
+ only on full success. A compiler/IR/adapter upgrade needs the user's explicit
46
+ approval of the exact delta.
47
+
48
+ ## Repository sync (CLI)
49
+
50
+ The same deterministic runner ships as a CLI:
51
+
52
+ ```bash
53
+ # preview (no writes)
54
+ SHIPPP_API_KEY=sk_... npx @shippp/mcp plan --root .
55
+
56
+ # full sync: apply + checks + receipt, with rollback
57
+ SHIPPP_API_KEY=sk_... npx @shippp/mcp sync --root . --check "npm run typecheck"
58
+ ```
59
+
60
+ Flags: `--root <repo>` (default: cwd), `--check <cmd>` (repeatable; runs after
61
+ apply, before the receipt), `--allow-compiler-upgrade` (explicit consent to a
62
+ pending compiler/IR/adapter delta), `--accept-rebound-baseline` (adopt the
63
+ previous binding's receipt as baseline after an approved re-bind). In server
64
+ mode the repo root comes from `SHIPPP_REPO_ROOT` (or the server's cwd) and the
65
+ check command from `SHIPPP_SYNC_CHECKS` — developer configuration, never tool
66
+ input. Exit codes: `0` success, `2` structured refusal, `1` error.
67
+
68
+ ## MCP Registry
69
+
70
+ Published to the official MCP Registry as `ai.shippp/mcp`.
package/dist/index.js CHANGED
@@ -1,20 +1,812 @@
1
1
  #!/usr/bin/env node
2
+
3
+ // src/index.ts
2
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { loadConfig } from "./config.js";
5
- import { ShipppClient } from "./client.js";
6
- import { registerTools } from "./tools.js";
7
- const INSTRUCTIONS = `Shippp exposes DETERMINISTIC, production-perfect Chakra UI code from a designer's Shippp file. Everything returned is GENERATED (never guessed) and reproducible — the same source always yields the same code + hash (see each result's receipt).
8
6
 
9
- Workflow: call shippp_list_units first to discover screen/master ids, then pull code with shippp_get_component / shippp_get_tokens / shippp_get_design_system / shippp_get_project. Write the returned "=== FILE: <path> ===" blocks to those paths. Do NOT hand-edit generated code to reflect a design change — change the design in Shippp and regenerate. Use shippp_diff to see exactly what a designer changed since your current code.`;
7
+ // src/config.ts
8
+ var DEFAULT_API_URL = "https://app.shippp.ai";
9
+ function loadConfig() {
10
+ const apiKey = process.env.SHIPPP_API_KEY?.trim();
11
+ if (!apiKey) {
12
+ console.error(
13
+ "[shippp-mcp] SHIPPP_API_KEY is not set. Mint a key in your Shippp workspace settings and add it to the MCP server's `env` in your IDE config."
14
+ );
15
+ process.exit(1);
16
+ }
17
+ const apiUrl = (process.env.SHIPPP_API_URL?.trim() || DEFAULT_API_URL).replace(/\/+$/, "");
18
+ return { apiUrl, apiKey };
19
+ }
20
+
21
+ // src/client.ts
22
+ var ShipppApiError = class extends Error {
23
+ constructor(status, message) {
24
+ super(message);
25
+ this.status = status;
26
+ this.name = "ShipppApiError";
27
+ }
28
+ };
29
+ function decodeFiles(files) {
30
+ return files.map(
31
+ (f) => f.encoding === "base64" ? { path: f.path, content: new Uint8Array(Buffer.from(f.content, "base64")) } : { path: f.path, content: f.content }
32
+ );
33
+ }
34
+ var ShipppClient = class {
35
+ constructor(config) {
36
+ this.config = config;
37
+ }
38
+ async request(path2, init) {
39
+ let res;
40
+ try {
41
+ res = await fetch(`${this.config.apiUrl}${path2}`, {
42
+ ...init,
43
+ headers: {
44
+ Authorization: `Bearer ${this.config.apiKey}`,
45
+ "Content-Type": "application/json",
46
+ ...init.headers ?? {}
47
+ }
48
+ });
49
+ } catch (e) {
50
+ throw new ShipppApiError(0, `Could not reach Shippp at ${this.config.apiUrl}: ${e.message}`);
51
+ }
52
+ const body = await res.json().catch(() => ({}));
53
+ if (!res.ok) throw new ShipppApiError(res.status, body?.error || `HTTP ${res.status}`);
54
+ return body;
55
+ }
56
+ /** List a file's screens + master components (id + name). */
57
+ manifest(fileId2) {
58
+ return this.request(`/api/mcp/manifest?fileId=${encodeURIComponent(fileId2)}`, { method: "GET" });
59
+ }
60
+ /** Generate code for a unit (or a diff against a baseline). */
61
+ async generate(fileId2, request, baseline) {
62
+ const body = await this.request(
63
+ "/api/mcp/generate",
64
+ {
65
+ method: "POST",
66
+ body: JSON.stringify(baseline ? { fileId: fileId2, request, baseline } : { fileId: fileId2, request })
67
+ }
68
+ );
69
+ if ("files" in body) return { ...body, files: decodeFiles(body.files) };
70
+ return body;
71
+ }
72
+ };
73
+
74
+ // src/tools.ts
75
+ import { z as z3 } from "zod";
76
+
77
+ // src/runner.ts
78
+ import { exec } from "node:child_process";
79
+ import { promisify } from "node:util";
80
+
81
+ // ../../src/lib/repoSync/applySync.ts
82
+ import { createHash } from "node:crypto";
83
+
84
+ // ../../src/lib/repoSync/targetConfig.ts
85
+ import { z } from "zod";
86
+ var TARGET_CONFIG_PATH = "shippp.target.json";
87
+ var TARGET_CONFIG_SCHEMA_VERSION = 1;
88
+ var DEFAULT_SYNC_UNIT = "project";
89
+ var targetConfigSchema = z.object({
90
+ schemaVersion: z.literal(TARGET_CONFIG_SCHEMA_VERSION),
91
+ profile: z.string().min(1),
92
+ profileSchemaVersion: z.number().int().positive(),
93
+ generatedRoot: z.string().min(1),
94
+ project: z.object({ fileId: z.string().min(1), name: z.string().min(1) }).optional(),
95
+ unit: z.enum(["project", "design-system"]).optional()
96
+ });
97
+ var TargetConfigError = class extends Error {
98
+ };
99
+ function isSafeRelativePath(p) {
100
+ if (p.length === 0 || p.startsWith("/") || p.includes("\\")) return false;
101
+ const segments = p.split("/");
102
+ return segments.every((s) => s.length > 0 && s !== "." && s !== "..");
103
+ }
104
+ function parseTargetConfig(raw) {
105
+ let json;
106
+ try {
107
+ json = JSON.parse(raw);
108
+ } catch {
109
+ throw new TargetConfigError(`A ${TARGET_CONFIG_PATH} nem \xE9rv\xE9nyes JSON.`);
110
+ }
111
+ const parsed = targetConfigSchema.safeParse(json);
112
+ if (!parsed.success) {
113
+ throw new TargetConfigError(
114
+ `A ${TARGET_CONFIG_PATH} nem felel meg a(z) ${TARGET_CONFIG_SCHEMA_VERSION}. s\xE9maverzi\xF3nak: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
115
+ );
116
+ }
117
+ if (!isSafeRelativePath(parsed.data.generatedRoot)) {
118
+ throw new TargetConfigError(`A generatedRoot nem biztons\xE1gos repo-relat\xEDv \xFAtvonal: "${parsed.data.generatedRoot}".`);
119
+ }
120
+ return parsed.data;
121
+ }
122
+
123
+ // ../../src/lib/repoSync/receiptContract.ts
124
+ import { z as z2 } from "zod";
125
+ var hash = z2.string().regex(/^sha256:[0-9a-f]{64}$/);
126
+ var receiptFileEntrySchema = z2.object({
127
+ path: z2.string().min(1),
128
+ hash,
129
+ bytes: z2.number().int().nonnegative()
130
+ });
131
+ var syncReceiptSchema = z2.object({
132
+ shippp_receipt: z2.literal("v2"),
133
+ source: z2.object({ file: z2.string(), unit: z2.string(), version: hash }),
134
+ target: z2.object({
135
+ outputProfile: z2.string().min(1),
136
+ profileSchemaVersion: z2.number().int().positive(),
137
+ adapterVersion: z2.string().min(1),
138
+ packagerVersion: z2.string().min(1),
139
+ packages: z2.object({
140
+ dependencies: z2.record(z2.string(), z2.string()),
141
+ devDependencies: z2.record(z2.string(), z2.string())
142
+ })
143
+ }),
144
+ config: z2.object({ engine: z2.string().min(1), irVersion: z2.string().min(1), tokens: hash }),
145
+ output: z2.object({ hash, files: z2.array(receiptFileEntrySchema) }),
146
+ reproducible: z2.literal(true),
147
+ generated_at: z2.string()
148
+ });
149
+ var ReceiptParseError = class extends Error {
150
+ };
151
+ function parseSyncReceipt(raw) {
152
+ let json;
153
+ try {
154
+ json = JSON.parse(raw);
155
+ } catch {
156
+ throw new ReceiptParseError("A receipt nem \xE9rv\xE9nyes JSON.");
157
+ }
158
+ const parsed = syncReceiptSchema.safeParse(json);
159
+ if (!parsed.success) {
160
+ throw new ReceiptParseError(
161
+ `A receipt nem felel meg a v2 kontraktusnak: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
162
+ );
163
+ }
164
+ return parsed.data;
165
+ }
166
+ function serializeSyncReceipt(receipt) {
167
+ return `${JSON.stringify(receipt, null, 2)}
168
+ `;
169
+ }
170
+ var compilerEnvOfReceipt = (r) => ({
171
+ engine: r.config.engine,
172
+ irVersion: r.config.irVersion,
173
+ adapterVersion: r.target.adapterVersion,
174
+ packagerVersion: r.target.packagerVersion
175
+ });
176
+ function compilerDeltaOf(previous, next) {
177
+ const delta = [];
178
+ if (previous.config.engine !== next.engine) delta.push(`engine: ${previous.config.engine} \u2192 ${next.engine}`);
179
+ if (previous.config.irVersion !== next.irVersion) delta.push(`IR: ${previous.config.irVersion} \u2192 ${next.irVersion}`);
180
+ if (previous.target.adapterVersion !== next.adapterVersion) {
181
+ delta.push(`adapter: ${previous.target.adapterVersion} \u2192 ${next.adapterVersion}`);
182
+ }
183
+ if (previous.target.packagerVersion !== next.packagerVersion) {
184
+ delta.push(`packager: ${previous.target.packagerVersion} \u2192 ${next.packagerVersion}`);
185
+ }
186
+ return delta;
187
+ }
188
+
189
+ // ../../src/lib/repoSync/syncPlan.ts
190
+ function computeSyncPlan(input) {
191
+ const entries = [];
192
+ const conflicts = [];
193
+ const paths = [.../* @__PURE__ */ new Set([...input.artifact.keys(), ...input.previous?.keys() ?? []])].sort();
194
+ for (const path2 of paths) {
195
+ const next = input.artifact.get(path2);
196
+ const prev = input.previous?.get(path2);
197
+ const local = input.local.get(path2) ?? null;
198
+ if (next !== void 0) {
199
+ if (local === null) {
200
+ entries.push({ path: path2, action: "create" });
201
+ } else if (local === next) {
202
+ entries.push({ path: path2, action: "unchanged" });
203
+ } else if (prev !== void 0 && local === prev) {
204
+ entries.push({ path: path2, action: "update" });
205
+ } else if (prev !== void 0) {
206
+ conflicts.push({
207
+ path: path2,
208
+ kind: "modified",
209
+ message: `A gener\xE1lt f\xE1jl k\xE9zzel m\xF3dosult: ${path2} \u2014 a sync nem \xEDrja fel\xFCl. Felold\xE1s: snapshotv\xE1laszt\xE1s \xE9s \xFAjragener\xE1l\xE1s, nem k\xE9zi merge.`
210
+ });
211
+ } else {
212
+ conflicts.push({
213
+ path: path2,
214
+ kind: "untracked",
215
+ message: `Nem Shippp-syncelt lok\xE1lis f\xE1jl \xE1ll a gener\xE1lt \xFAtvonalon: ${path2} \u2014 a sync nem \xEDrja fel\xFCl.`
216
+ });
217
+ }
218
+ } else if (prev !== void 0) {
219
+ if (local === null) {
220
+ entries.push({ path: path2, action: "unchanged" });
221
+ } else if (local === prev) {
222
+ entries.push({ path: path2, action: "delete" });
223
+ } else {
224
+ conflicts.push({
225
+ path: path2,
226
+ kind: "modified-delete",
227
+ message: `A t\xF6rlend\u0151 gener\xE1lt f\xE1jl k\xE9zzel m\xF3dosult: ${path2} \u2014 a sync nem t\xF6rli.`
228
+ });
229
+ }
230
+ }
231
+ }
232
+ return { entries, conflicts };
233
+ }
234
+
235
+ // ../../src/lib/repoSync/applySync.ts
236
+ var RECEIPT_FILENAME = "shippp.receipt.json";
237
+ var sha256 = (content) => `sha256:${createHash("sha256").update(content).digest("hex")}`;
238
+ var refusal = (code, message, conflicts) => ({
239
+ status: "refused",
240
+ code,
241
+ message,
242
+ ...conflicts ? { conflicts } : {}
243
+ });
244
+ function artifactIntegrityIssue(artifact) {
245
+ const byPath = new Map(artifact.files.map((f) => [f.path, f]));
246
+ if (byPath.size !== artifact.files.length) return "az artifact f\xE1jllist\xE1j\xE1ban duplik\xE1lt \xFAtvonal van";
247
+ const mapped = artifact.receipt.output.files;
248
+ if (mapped.length !== artifact.files.length) return "a receipt f\xE1jl-mappingje \xE9s az artifact f\xE1jlk\xE9szlete elt\xE9r";
249
+ for (const entry of mapped) {
250
+ if (!isSafeRelativePath(entry.path)) return `nem biztons\xE1gos artifact-\xFAtvonal: "${entry.path}"`;
251
+ if (entry.path === RECEIPT_FILENAME) return `az artifact nem tartalmazhat ${RECEIPT_FILENAME} f\xE1jlt`;
252
+ const file = byPath.get(entry.path);
253
+ if (!file) return `a receipt olyan f\xE1jlt c\xEDmez, amely nincs az artifactban: ${entry.path}`;
254
+ if (sha256(file.content) !== entry.hash) return `az artifact-tartalom nem egyezik a receipt hash-\xE9vel: ${entry.path}`;
255
+ }
256
+ return null;
257
+ }
258
+ async function resolveContext(fs, artifact, options = {}) {
259
+ const rawConfig = await fs.read(TARGET_CONFIG_PATH);
260
+ if (rawConfig === null) {
261
+ return refusal(
262
+ "no-target-config",
263
+ `Nincs ${TARGET_CONFIG_PATH} a repositoryban \u2014 a target be\xE1ll\xEDt\xE1sa explicit setup-l\xE9p\xE9s, a sync nem hozza l\xE9tre.`
264
+ );
265
+ }
266
+ let config;
267
+ try {
268
+ config = parseTargetConfig(Buffer.from(rawConfig).toString("utf8"));
269
+ } catch (e) {
270
+ if (e instanceof TargetConfigError) return refusal("invalid-target-config", e.message);
271
+ throw e;
272
+ }
273
+ const integrity = artifactIntegrityIssue(artifact);
274
+ if (integrity) return refusal("artifact-integrity", `Az artifact nem konzisztens: ${integrity}.`);
275
+ if (config.project && artifact.receipt.source.file !== config.project.fileId) {
276
+ return refusal(
277
+ "project-mismatch",
278
+ `Az artifact forr\xE1sa (${artifact.receipt.source.file}) nem a repository k\xF6t\xF6tt projektje (${config.project.fileId}) \u2014 \xE1tk\xF6t\xE9s kiz\xE1r\xF3lag a bind explicit \xFAtj\xE1n t\xF6rt\xE9nhet.`
279
+ );
280
+ }
281
+ const configUnit = config.unit ?? DEFAULT_SYNC_UNIT;
282
+ if (artifact.receipt.source.unit !== configUnit) {
283
+ return refusal(
284
+ "unit-mismatch",
285
+ `Az artifact sync-egys\xE9ge (${artifact.receipt.source.unit}) nem a repository k\xF6t\xF6tt egys\xE9ge (${configUnit}).`
286
+ );
287
+ }
288
+ if (artifact.receipt.target.outputProfile !== config.profile) {
289
+ return refusal(
290
+ "profile-mismatch",
291
+ `Az artifact profilja (${artifact.receipt.target.outputProfile}) nem a repository explicit default profilja (${config.profile}) \u2014 a profilv\xE1lt\xE1s k\xFCl\xF6n, explicit folyamat.`
292
+ );
293
+ }
294
+ if (artifact.receipt.target.profileSchemaVersion !== config.profileSchemaVersion) {
295
+ return refusal(
296
+ "profile-schema-mismatch",
297
+ `Az artifact profile schema verzi\xF3ja (${artifact.receipt.target.profileSchemaVersion}) elt\xE9r a repositoryban r\xF6gz\xEDtett\u0151l (${config.profileSchemaVersion}).`
298
+ );
299
+ }
300
+ const receiptPath = `${config.generatedRoot}/${RECEIPT_FILENAME}`;
301
+ const rawReceipt = await fs.read(receiptPath);
302
+ let previousReceipt = null;
303
+ if (rawReceipt !== null) {
304
+ try {
305
+ previousReceipt = parseSyncReceipt(Buffer.from(rawReceipt).toString("utf8"));
306
+ } catch (e) {
307
+ if (e instanceof ReceiptParseError) return refusal("invalid-previous-receipt", e.message);
308
+ throw e;
309
+ }
310
+ }
311
+ if (previousReceipt && (previousReceipt.source.unit !== configUnit || config.project && previousReceipt.source.file !== config.project.fileId) && options.acceptReboundBaseline !== true) {
312
+ return refusal(
313
+ "receipt-binding-mismatch",
314
+ `A repository receiptje (${previousReceipt.source.file} / ${previousReceipt.source.unit}) nem a jelenlegi bindinghoz tartozik \u2014 \xE1tk\xF6t\xE9s ut\xE1ni els\u0151 synchez az acceptReboundBaseline explicit d\xF6nt\xE9se kell; a k\xE9zzel m\xF3dos\xEDtott f\xE1jlok v\xE9delme mellette is \xE1ll.`
315
+ );
316
+ }
317
+ const repoPathOf = (artifactPath) => `${config.generatedRoot}/${artifactPath}`;
318
+ const artifactHashes = new Map(artifact.receipt.output.files.map((e) => [e.path, e.hash]));
319
+ const previousHashes = previousReceipt ? new Map(previousReceipt.output.files.map((e) => [e.path, e.hash])) : null;
320
+ const local = /* @__PURE__ */ new Map();
321
+ for (const path2 of /* @__PURE__ */ new Set([...artifactHashes.keys(), ...previousHashes?.keys() ?? []])) {
322
+ const bytes = await fs.read(repoPathOf(path2));
323
+ local.set(path2, bytes === null ? null : sha256(bytes));
324
+ }
325
+ const plan = computeSyncPlan({ artifact: artifactHashes, previous: previousHashes, local });
326
+ return { config, previousReceipt, plan, receiptPath, repoPathOf, localAtPlan: local };
327
+ }
328
+ async function planRepoSync(fs, artifact, options = {}) {
329
+ const ctx = await resolveContext(fs, artifact, options);
330
+ if ("status" in ctx) return ctx;
331
+ return { status: "plan", config: ctx.config, plan: ctx.plan, previousReceipt: ctx.previousReceipt };
332
+ }
333
+ async function applyRepoSync(fs, artifact, options = {}) {
334
+ const ctx = await resolveContext(fs, artifact, options);
335
+ if ("status" in ctx) return ctx;
336
+ const { plan, previousReceipt, receiptPath, repoPathOf, localAtPlan } = ctx;
337
+ if (previousReceipt) {
338
+ const delta = compilerDeltaOf(previousReceipt, compilerEnvOfReceipt(artifact.receipt));
339
+ if (delta.length > 0 && !options.allowCompilerUpgrade) {
340
+ return refusal(
341
+ "compiler-upgrade-requires-approval",
342
+ `A compiler-k\xF6rnyezet v\xE1ltozott (${delta.join(", ")}) \u2014 megl\xE9v\u0151 repository nem kaphat \xFAj verzi\xF3t csendben; explicit j\xF3v\xE1hagy\xE1s sz\xFCks\xE9ges.`
343
+ );
344
+ }
345
+ }
346
+ if (plan.conflicts.length > 0) {
347
+ return refusal(
348
+ "conflict",
349
+ `A sync ${plan.conflicts.length} konfliktus miatt nem alkalmazhat\xF3; semmi nem \xEDr\xF3dott.`,
350
+ plan.conflicts
351
+ );
352
+ }
353
+ const contentByPath = new Map(artifact.files.map((f) => [f.path, f.content]));
354
+ const written = [];
355
+ const deleted = [];
356
+ const backups = /* @__PURE__ */ new Map();
357
+ const rollback = async () => {
358
+ for (const [repoPath, original] of backups) {
359
+ if (original === null) await fs.remove(repoPath);
360
+ else await fs.write(repoPath, original);
361
+ }
362
+ };
363
+ try {
364
+ for (const entry of plan.entries) {
365
+ if (entry.action === "unchanged") continue;
366
+ const repoPath = repoPathOf(entry.path);
367
+ const currentBytes = await fs.read(repoPath);
368
+ const currentHash = currentBytes === null ? null : sha256(currentBytes);
369
+ if (currentHash !== (localAtPlan.get(entry.path) ?? null)) {
370
+ await rollback();
371
+ return refusal(
372
+ "apply-raced",
373
+ `A lemez a terv \xE9s az alkalmaz\xE1s k\xF6z\xF6tt megv\xE1ltozott (${entry.path}) \u2014 minden kor\xE1bbi v\xE1ltoz\xE1s visszag\xF6rgetve; futtasd \xFAjra a syncet friss tervvel.`
374
+ );
375
+ }
376
+ backups.set(repoPath, currentBytes);
377
+ if (entry.action === "create" || entry.action === "update") {
378
+ await fs.write(repoPath, contentByPath.get(entry.path));
379
+ written.push(entry.path);
380
+ } else {
381
+ await fs.remove(repoPath);
382
+ deleted.push(entry.path);
383
+ }
384
+ }
385
+ } catch (e) {
386
+ await rollback();
387
+ return refusal("apply-failed", `Az alkalmaz\xE1s hib\xE1ra futott, minden v\xE1ltoz\xE1s visszag\xF6rgetve: ${e.message}`);
388
+ }
389
+ if (options.runChecks) {
390
+ try {
391
+ await options.runChecks();
392
+ } catch (e) {
393
+ await rollback();
394
+ return refusal(
395
+ "checks-failed",
396
+ `A sync ellen\u0151rz\xE9sei hib\xE1ztak, minden v\xE1ltoz\xE1s visszag\xF6rgetve; a receipt nem friss\xFClt: ${e.message}`
397
+ );
398
+ }
399
+ }
400
+ try {
401
+ await fs.write(receiptPath, serializeSyncReceipt(artifact.receipt));
402
+ } catch (e) {
403
+ await rollback();
404
+ return refusal("apply-failed", `A receipt \xEDr\xE1sa hib\xE1ra futott, minden v\xE1ltoz\xE1s visszag\xF6rgetve: ${e.message}`);
405
+ }
406
+ return { status: "applied", plan, written, deleted, receiptPath };
407
+ }
408
+
409
+ // ../../src/lib/repoSync/nodeFs.ts
410
+ import { promises as fsp } from "node:fs";
411
+ import path from "node:path";
412
+ function nodeSyncFs(rootDir) {
413
+ const resolve = (p) => {
414
+ const full = path.resolve(rootDir, p);
415
+ if (full !== rootDir && !full.startsWith(rootDir + path.sep)) {
416
+ throw new Error(`A(z) "${p}" \xFAtvonal a repo-gy\xF6k\xE9ren k\xEDv\xFClre mutat.`);
417
+ }
418
+ return full;
419
+ };
420
+ return {
421
+ async read(p) {
422
+ try {
423
+ return new Uint8Array(await fsp.readFile(resolve(p)));
424
+ } catch (e) {
425
+ if (e.code === "ENOENT") return null;
426
+ throw e;
427
+ }
428
+ },
429
+ async write(p, content) {
430
+ const full = resolve(p);
431
+ await fsp.mkdir(path.dirname(full), { recursive: true });
432
+ await fsp.writeFile(full, content);
433
+ },
434
+ async remove(p) {
435
+ await fsp.rm(resolve(p), { force: true });
436
+ }
437
+ };
438
+ }
439
+
440
+ // src/runner.ts
441
+ var execAsync = promisify(exec);
442
+ var refusalOutcome = (r, compilerDelta) => ({
443
+ status: "refused",
444
+ code: r.code,
445
+ message: r.message,
446
+ ...r.conflicts ? { conflicts: r.conflicts } : {},
447
+ ...compilerDelta && compilerDelta.length > 0 ? { compilerDelta } : {}
448
+ });
449
+ async function fetchContext(client, root) {
450
+ const fs = nodeSyncFs(root);
451
+ const rawConfig = await fs.read(TARGET_CONFIG_PATH);
452
+ if (rawConfig === null) {
453
+ return {
454
+ status: "refused",
455
+ code: "no-target-config",
456
+ message: `Nincs ${TARGET_CONFIG_PATH} a repositoryban \u2014 a target be\xE1ll\xEDt\xE1sa explicit setup-l\xE9p\xE9s (shippp_bind_repository), a sync nem hozza l\xE9tre.`
457
+ };
458
+ }
459
+ let config;
460
+ try {
461
+ config = parseTargetConfig(Buffer.from(rawConfig).toString("utf8"));
462
+ } catch (e) {
463
+ if (e instanceof TargetConfigError) return { status: "refused", code: "invalid-target-config", message: e.message };
464
+ throw e;
465
+ }
466
+ if (!config.project) {
467
+ return {
468
+ status: "refused",
469
+ code: "no-project-binding",
470
+ message: `A ${TARGET_CONFIG_PATH} nem tartalmaz projekt-bindingot \u2014 futtasd a shippp_bind_repository k\xF6t\xE9st (explicit setup-l\xE9p\xE9s).`
471
+ };
472
+ }
473
+ const unit = config.unit ?? DEFAULT_SYNC_UNIT;
474
+ const result = await client.generate(config.project.fileId, { unit, profile: config.profile });
475
+ if (!result || !Array.isArray(result.files) || !result.receipt) {
476
+ return { status: "refused", code: "invalid-artifact", message: "A backend v\xE1lasza nem gener\xE1l\xE1si artifact." };
477
+ }
478
+ let artifact;
479
+ try {
480
+ artifact = { files: result.files, receipt: parseSyncReceipt(JSON.stringify(result.receipt)) };
481
+ } catch (e) {
482
+ if (e instanceof ReceiptParseError) return { status: "refused", code: "invalid-artifact", message: e.message };
483
+ throw e;
484
+ }
485
+ let compilerDelta = [];
486
+ const rawPrevious = await fs.read(`${config.generatedRoot}/${RECEIPT_FILENAME}`);
487
+ if (rawPrevious !== null) {
488
+ try {
489
+ const previous = parseSyncReceipt(Buffer.from(rawPrevious).toString("utf8"));
490
+ compilerDelta = compilerDeltaOf(previous, compilerEnvOfReceipt(artifact.receipt));
491
+ } catch {
492
+ }
493
+ }
494
+ return { config, artifact, compilerDelta };
495
+ }
496
+ function syncOptionsOf(opts, compilerDelta) {
497
+ const approved = Array.isArray(opts.allowCompilerUpgrade) ? compilerDelta.length > 0 && opts.allowCompilerUpgrade.length === compilerDelta.length && opts.allowCompilerUpgrade.every((line, i) => line === compilerDelta[i]) : opts.allowCompilerUpgrade === true;
498
+ return {
499
+ allowCompilerUpgrade: approved,
500
+ acceptReboundBaseline: opts.acceptReboundBaseline === true
501
+ };
502
+ }
503
+ async function planRepo(client, opts) {
504
+ const ctx = await fetchContext(client, opts.root);
505
+ if ("status" in ctx) return ctx;
506
+ const preview = await planRepoSync(nodeSyncFs(opts.root), ctx.artifact, syncOptionsOf(opts, ctx.compilerDelta));
507
+ if (preview.status === "refused") return refusalOutcome(preview, ctx.compilerDelta);
508
+ return {
509
+ status: "plan",
510
+ fileId: ctx.config.project.fileId,
511
+ unit: ctx.config.unit ?? DEFAULT_SYNC_UNIT,
512
+ profile: ctx.config.profile,
513
+ generatedRoot: ctx.config.generatedRoot,
514
+ entries: preview.plan.entries.filter((e) => e.action !== "unchanged"),
515
+ conflicts: preview.plan.conflicts,
516
+ compilerDelta: ctx.compilerDelta
517
+ };
518
+ }
519
+ async function applyRepo(client, opts) {
520
+ const ctx = await fetchContext(client, opts.root);
521
+ if ("status" in ctx) return ctx;
522
+ const checks = opts.checks ?? [];
523
+ const result = await applyRepoSync(nodeSyncFs(opts.root), ctx.artifact, {
524
+ ...syncOptionsOf(opts, ctx.compilerDelta),
525
+ runChecks: checks.length > 0 ? async () => {
526
+ for (const cmd of checks) {
527
+ try {
528
+ await execAsync(cmd, { cwd: opts.root, maxBuffer: 16 * 1024 * 1024 });
529
+ } catch (e) {
530
+ const err = e;
531
+ const tail = String(err.stderr || err.stdout || err.message || "").slice(-2e3);
532
+ throw new Error(`${cmd}
533
+ ${tail}`);
534
+ }
535
+ }
536
+ } : void 0
537
+ });
538
+ if (result.status === "refused") return refusalOutcome(result, ctx.compilerDelta);
539
+ return {
540
+ status: "applied",
541
+ fileId: ctx.config.project.fileId,
542
+ unit: ctx.config.unit ?? DEFAULT_SYNC_UNIT,
543
+ profile: ctx.config.profile,
544
+ generatedRoot: ctx.config.generatedRoot,
545
+ written: result.written,
546
+ deleted: result.deleted,
547
+ receiptPath: result.receiptPath,
548
+ checksRan: checks
549
+ };
550
+ }
551
+
552
+ // src/tools.ts
553
+ var text = (s) => ({ content: [{ type: "text", text: s }] });
554
+ var fail = (s) => ({ content: [{ type: "text", text: s }], isError: true });
555
+ function errText(e) {
556
+ if (e instanceof ShipppApiError) {
557
+ const hint = e.status === 401 ? " \u2014 check SHIPPP_API_KEY." : "";
558
+ return `Error ${e.status}: ${e.message}${hint}`;
559
+ }
560
+ return `Error: ${e.message}`;
561
+ }
562
+ function renderFiles(result) {
563
+ const files = result.files.map(
564
+ (f) => typeof f.content === "string" ? `=== FILE: ${f.path} ===
565
+ ${f.content}` : `=== FILE: ${f.path} === (binary, ${f.content.byteLength} bytes \u2014 fetch via sync, not chat)`
566
+ ).join("\n\n");
567
+ return `${files}
568
+
569
+ --- RECEIPT (reproducible: ${result.receipt.reproducible}, engine: ${result.receipt.config.engine}) ---
570
+ ${JSON.stringify(result.receipt, null, 2)}`;
571
+ }
572
+ function repoRootOf(env = process.env) {
573
+ return env.SHIPPP_REPO_ROOT?.trim() || process.cwd();
574
+ }
575
+ function checksOf(env = process.env) {
576
+ const cmd = env.SHIPPP_SYNC_CHECKS?.trim();
577
+ return cmd ? [cmd] : [];
578
+ }
579
+ function renderDiff(result) {
580
+ const changed = result.changed_files.map((f) => `${f.status} ${f.path}`).join("\n");
581
+ return `changed: ${result.changed}
582
+
583
+ ${result.diff || "(no changes)"}
584
+
585
+ --- files ---
586
+ ${changed}
587
+
588
+ --- RECEIPT ---
589
+ ${JSON.stringify(result.receipt, null, 2)}`;
590
+ }
591
+ var fileId = z3.string().min(1).describe("The Shippp file id (from the file's URL, or shippp_list_units).");
592
+ function registerTools(server, client) {
593
+ server.registerTool(
594
+ "shippp_list_units",
595
+ {
596
+ annotations: { readOnlyHint: true, openWorldHint: false },
597
+ description: "List a Shippp file's screens and master components (id + name). Call this FIRST to discover the ids for shippp_get_component.",
598
+ inputSchema: { fileId }
599
+ },
600
+ async ({ fileId: fileId2 }) => {
601
+ try {
602
+ const { manifest } = await client.manifest(fileId2);
603
+ return text(JSON.stringify(manifest, null, 2));
604
+ } catch (e) {
605
+ return fail(errText(e));
606
+ }
607
+ }
608
+ );
609
+ server.registerTool(
610
+ "shippp_get_component",
611
+ {
612
+ annotations: { readOnlyHint: true, openWorldHint: false },
613
+ description: "Get ONE component's deterministic Chakra UI code + a reproducibility receipt. `unitType` is 'master' (a design-system component) or 'screen' (a depth-0 artboard). Get ids from shippp_list_units.",
614
+ inputSchema: {
615
+ fileId,
616
+ unitId: z3.string().min(1).describe("The master/screen id from shippp_list_units."),
617
+ unitType: z3.enum(["master", "screen"])
618
+ }
619
+ },
620
+ async ({ fileId: fileId2, unitId, unitType }) => {
621
+ try {
622
+ return text(renderFiles(await client.generate(fileId2, { unit: "component", unitId, unitType })));
623
+ } catch (e) {
624
+ return fail(errText(e));
625
+ }
626
+ }
627
+ );
628
+ server.registerTool(
629
+ "shippp_get_tokens",
630
+ {
631
+ annotations: { readOnlyHint: true, openWorldHint: false },
632
+ description: "Get the design system's tokens as a Chakra theme (createSystem) + receipt.",
633
+ inputSchema: { fileId }
634
+ },
635
+ async ({ fileId: fileId2 }) => {
636
+ try {
637
+ return text(renderFiles(await client.generate(fileId2, { unit: "tokens" })));
638
+ } catch (e) {
639
+ return fail(errText(e));
640
+ }
641
+ }
642
+ );
643
+ server.registerTool(
644
+ "shippp_get_design_system",
645
+ {
646
+ annotations: { readOnlyHint: true, openWorldHint: false },
647
+ description: "Get the whole design system as a publishable package (tokens + theme + recipes + every component + stories + package.json).",
648
+ inputSchema: { fileId }
649
+ },
650
+ async ({ fileId: fileId2 }) => {
651
+ try {
652
+ return text(renderFiles(await client.generate(fileId2, { unit: "design-system" })));
653
+ } catch (e) {
654
+ return fail(errText(e));
655
+ }
656
+ }
657
+ );
658
+ server.registerTool(
659
+ "shippp_get_project",
660
+ {
661
+ annotations: { readOnlyHint: true, openWorldHint: false },
662
+ description: "Get the whole file as a runnable Next.js project (screens as pages + the design system).",
663
+ inputSchema: { fileId }
664
+ },
665
+ async ({ fileId: fileId2 }) => {
666
+ try {
667
+ return text(renderFiles(await client.generate(fileId2, { unit: "project" })));
668
+ } catch (e) {
669
+ return fail(errText(e));
670
+ }
671
+ }
672
+ );
673
+ server.registerTool(
674
+ "shippp_diff",
675
+ {
676
+ annotations: { readOnlyHint: true, openWorldHint: false },
677
+ description: "Diff your CURRENT code against a fresh deterministic generation of a unit \u2014 shows exactly what a designer changed. Pass the same unit params as the get tools, plus `baseline` (the files you currently have for that unit).",
678
+ inputSchema: {
679
+ fileId,
680
+ unit: z3.enum(["tokens", "design-system", "project", "component"]),
681
+ unitId: z3.string().optional().describe("Required when unit='component'."),
682
+ unitType: z3.enum(["master", "screen"]).optional().describe("Required when unit='component'."),
683
+ baseline: z3.array(z3.object({ path: z3.string(), content: z3.string() })).describe("Your current files for this unit (path + content).")
684
+ }
685
+ },
686
+ async ({ fileId: fileId2, unit, unitId, unitType, baseline }) => {
687
+ try {
688
+ let request;
689
+ if (unit === "component") {
690
+ if (!unitId || !unitType) return fail("unit='component' requires unitId + unitType.");
691
+ request = { unit, unitId, unitType };
692
+ } else {
693
+ request = { unit };
694
+ }
695
+ return text(renderDiff(await client.generate(fileId2, request, baseline)));
696
+ } catch (e) {
697
+ return fail(errText(e));
698
+ }
699
+ }
700
+ );
701
+ server.registerTool(
702
+ "shippp_sync_plan",
703
+ {
704
+ annotations: { readOnlyHint: true, openWorldHint: false },
705
+ description: "Preview the repository's Shippp sync as a deterministic plan \u2014 NO writes. The repo root comes from the developer's own configuration (SHIPPP_REPO_ROOT env, or the server's working directory), never from tool input. Requires a committed shippp.target.json binding (created via shippp_bind_repository on the remote MCP). Returns the create/update/delete entries, hand-modified-file conflicts and any pending compiler/IR/adapter delta that shippp_sync_apply would need explicit approval for.",
706
+ inputSchema: {}
707
+ },
708
+ async () => {
709
+ try {
710
+ const outcome = await planRepo(client, { root: repoRootOf() });
711
+ const body = JSON.stringify(outcome, null, 2);
712
+ return outcome.status === "refused" ? fail(body) : text(body);
713
+ } catch (e) {
714
+ return fail(errText(e));
715
+ }
716
+ }
717
+ );
718
+ server.registerTool(
719
+ "shippp_sync_apply",
720
+ {
721
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
722
+ description: "Run the full repository sync with the bundled deterministic runner: fetch the bound unit's artifact, plan against the real on-disk state, apply atomically with rollback, run the repo's checks (SHIPPP_SYNC_CHECKS env), and write the receipt ONLY on full success. Writes are confined to the generated root of the repo's own binding; hand-modified generated files are structured conflicts and are never overwritten. If the plan or a refusal reports a compilerDelta, present it to the user and re-call with approvedCompilerDelta set to EXACTLY those lines ONLY after their explicit approval. After an approved re-bind, pass acceptReboundBaseline to adopt the previous binding's receipt as baseline.",
723
+ inputSchema: {
724
+ approvedCompilerDelta: z3.array(z3.string().min(1).max(500)).max(20).optional().describe(
725
+ "The exact compilerDelta lines the user explicitly approved \u2014 a mismatch with the current delta refuses."
726
+ ),
727
+ acceptReboundBaseline: z3.boolean().optional().describe("Explicit adoption of a previous binding's leftover receipt as hash baseline after a re-bind.")
728
+ }
729
+ },
730
+ async ({ approvedCompilerDelta, acceptReboundBaseline }) => {
731
+ try {
732
+ const outcome = await applyRepo(client, {
733
+ root: repoRootOf(),
734
+ allowCompilerUpgrade: approvedCompilerDelta,
735
+ acceptReboundBaseline,
736
+ checks: checksOf()
737
+ });
738
+ const body = JSON.stringify(outcome, null, 2);
739
+ return outcome.status === "refused" ? fail(body) : text(body);
740
+ } catch (e) {
741
+ return fail(errText(e));
742
+ }
743
+ }
744
+ );
745
+ }
746
+
747
+ // src/index.ts
748
+ var INSTRUCTIONS = `Shippp exposes DETERMINISTIC, production-perfect UI code generated from a designer's Shippp file. Everything returned is GENERATED (never guessed) and reproducible \u2014 the same source always yields the same code + hash (see each result's receipt).
749
+
750
+ Workflow: call shippp_list_units first to discover screen/master ids, then pull code with shippp_get_component / shippp_get_tokens / shippp_get_design_system / shippp_get_project. Write the returned "=== FILE: <path> ===" blocks to those paths. Do NOT hand-edit generated code to reflect a design change \u2014 change the design in Shippp and regenerate. Use shippp_diff to see exactly what a designer changed since your current code.
751
+
752
+ Keeping a repository current: once the repo has a committed shippp.target.json binding (created with shippp_bind_repository on the remote MCP, or written by hand), use shippp_sync_plan to preview and shippp_sync_apply to run the official receipt-tracked sync \u2014 do NOT re-pull whole projects and hand-diff them from chat memory. The runner never overwrites hand-modified generated files (structured conflict), never touches files outside the generated root, runs the repo's checks and writes the receipt only on full success. A compiler/IR/adapter delta needs the user's explicit approval (approvedCompilerDelta with the exact lines).`;
753
+ function parseCliFlags(rest) {
754
+ const opts = { checks: [] };
755
+ const errors = [];
756
+ for (let i = 0; i < rest.length; i++) {
757
+ const arg = rest[i];
758
+ const [flag, inlineValue] = arg.includes("=") ? [arg.slice(0, arg.indexOf("=")), arg.slice(arg.indexOf("=") + 1)] : [arg, void 0];
759
+ const value = () => inlineValue ?? rest[++i];
760
+ switch (flag) {
761
+ case "--root":
762
+ opts.root = value();
763
+ break;
764
+ case "--check":
765
+ opts.checks.push(value() ?? "");
766
+ break;
767
+ case "--allow-compiler-upgrade":
768
+ opts.allowCompilerUpgrade = true;
769
+ break;
770
+ case "--accept-rebound-baseline":
771
+ opts.acceptReboundBaseline = true;
772
+ break;
773
+ default:
774
+ errors.push(`Ismeretlen kapcsol\xF3: ${arg}`);
775
+ }
776
+ }
777
+ if (opts.checks.some((c) => !c)) errors.push("A --check kapcsol\xF3hoz parancs kell.");
778
+ return { opts, errors };
779
+ }
780
+ async function runCli(command, rest) {
781
+ const { opts, errors } = parseCliFlags(rest);
782
+ if (errors.length > 0) {
783
+ console.error(errors.join("\n"));
784
+ console.error("Haszn\xE1lat: shippp-mcp plan|sync [--root <repo>] [--check <cmd>]... [--allow-compiler-upgrade] [--accept-rebound-baseline]");
785
+ process.exit(1);
786
+ }
787
+ const client = new ShipppClient(loadConfig());
788
+ const root = opts.root ?? repoRootOf();
789
+ const outcome = command === "plan" ? await planRepo(client, { ...opts, root }) : await applyRepo(client, { ...opts, root });
790
+ console.log(JSON.stringify(outcome, null, 2));
791
+ process.exit(outcome.status === "refused" ? 2 : 0);
792
+ }
10
793
  async function main() {
11
- const config = loadConfig();
12
- const server = new McpServer({ name: "shippp", version: "0.1.0" }, { instructions: INSTRUCTIONS });
13
- registerTools(server, new ShipppClient(config));
14
- await server.connect(new StdioServerTransport());
15
- console.error(`[shippp-mcp] connected via stdio → ${config.apiUrl}`);
794
+ const [command, ...rest] = process.argv.slice(2);
795
+ if (command === "plan" || command === "sync") {
796
+ await runCli(command, rest);
797
+ return;
798
+ }
799
+ if (command !== void 0) {
800
+ console.error(`Ismeretlen parancs: ${command} \u2014 argumentum n\xE9lk\xFCl stdio MCP szerver indul; sync-hez: shippp-mcp plan|sync`);
801
+ process.exit(1);
802
+ }
803
+ const config = loadConfig();
804
+ const server = new McpServer({ name: "shippp", version: "0.2.0" }, { instructions: INSTRUCTIONS });
805
+ registerTools(server, new ShipppClient(config));
806
+ await server.connect(new StdioServerTransport());
807
+ console.error(`[shippp-mcp] connected via stdio \u2192 ${config.apiUrl}`);
16
808
  }
17
809
  main().catch((e) => {
18
- console.error("[shippp-mcp] fatal:", e);
19
- process.exit(1);
810
+ console.error("[shippp-mcp] fatal:", e);
811
+ process.exit(1);
20
812
  });
package/package.json CHANGED
@@ -1,17 +1,20 @@
1
1
  {
2
2
  "name": "@shippp/mcp",
3
- "version": "0.1.0",
4
- "description": "Shippp MCP server — pull deterministic, production-perfect Chakra UI code from your Shippp design files, straight into your IDE agent.",
3
+ "version": "0.2.0",
4
+ "mcpName": "ai.shippp/mcp",
5
+ "description": "Shippp MCP server + local sync runner — pull deterministic, production-perfect UI code from your Shippp design files, and keep your repository current over the receipt-tracked sync.",
5
6
  "type": "module",
6
7
  "bin": {
7
8
  "shippp-mcp": "dist/index.js"
8
9
  },
9
10
  "files": [
10
11
  "dist",
11
- "README.md"
12
+ "README.md",
13
+ "LICENSE"
12
14
  ],
13
15
  "scripts": {
14
- "build": "tsc",
16
+ "typecheck": "tsc --noEmit -p tsconfig.json",
17
+ "build": "npm run typecheck && esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@modelcontextprotocol/sdk --external:zod",
15
18
  "start": "node dist/index.js",
16
19
  "prepublishOnly": "rm -rf dist && npm run build"
17
20
  },
@@ -25,12 +28,20 @@
25
28
  "chakra-ui",
26
29
  "design-to-code"
27
30
  ],
28
- "license": "UNLICENSED",
31
+ "license": "MIT",
32
+ "homepage": "https://shippp.ai",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/Comet-Studio-Io/shippp.git",
36
+ "directory": "packages/mcp"
37
+ },
29
38
  "dependencies": {
30
39
  "@modelcontextprotocol/sdk": "^1.12.0",
31
40
  "zod": "^3.23.8"
32
41
  },
33
42
  "devDependencies": {
43
+ "@types/node": "^24.0.0",
44
+ "esbuild": "^0.24.0",
34
45
  "typescript": "^5.6.0"
35
46
  }
36
47
  }
package/dist/client.js DELETED
@@ -1,43 +0,0 @@
1
- export class ShipppApiError extends Error {
2
- status;
3
- constructor(status, message) {
4
- super(message);
5
- this.status = status;
6
- this.name = "ShipppApiError";
7
- }
8
- }
9
- export class ShipppClient {
10
- config;
11
- constructor(config) {
12
- this.config = config;
13
- }
14
- async request(path, init) {
15
- let res;
16
- try {
17
- res = await fetch(`${this.config.apiUrl}${path}`, {
18
- ...init,
19
- headers: {
20
- Authorization: `Bearer ${this.config.apiKey}`,
21
- "Content-Type": "application/json",
22
- ...(init.headers ?? {}),
23
- },
24
- });
25
- }
26
- catch (e) {
27
- throw new ShipppApiError(0, `Could not reach Shippp at ${this.config.apiUrl}: ${e.message}`);
28
- }
29
- const body = (await res.json().catch(() => ({})));
30
- if (!res.ok)
31
- throw new ShipppApiError(res.status, body?.error || `HTTP ${res.status}`);
32
- return body;
33
- }
34
- manifest(fileId) {
35
- return this.request(`/api/mcp/manifest?fileId=${encodeURIComponent(fileId)}`, { method: "GET" });
36
- }
37
- generate(fileId, request, baseline) {
38
- return this.request("/api/mcp/generate", {
39
- method: "POST",
40
- body: JSON.stringify(baseline ? { fileId, request, baseline } : { fileId, request }),
41
- });
42
- }
43
- }
package/dist/config.js DELETED
@@ -1,11 +0,0 @@
1
- const DEFAULT_API_URL = "https://app.shippp.ai";
2
- export function loadConfig() {
3
- const apiKey = process.env.SHIPPP_API_KEY?.trim();
4
- if (!apiKey) {
5
- console.error("[shippp-mcp] SHIPPP_API_KEY is not set. Mint a key in your Shippp workspace " +
6
- "settings and add it to the MCP server's `env` in your IDE config.");
7
- process.exit(1);
8
- }
9
- const apiUrl = (process.env.SHIPPP_API_URL?.trim() || DEFAULT_API_URL).replace(/\/+$/, "");
10
- return { apiUrl, apiKey };
11
- }
package/dist/tools.js DELETED
@@ -1,110 +0,0 @@
1
- import { z } from "zod";
2
- import { ShipppApiError } from "./client.js";
3
- const text = (s) => ({ content: [{ type: "text", text: s }] });
4
- const fail = (s) => ({ content: [{ type: "text", text: s }], isError: true });
5
- function errText(e) {
6
- if (e instanceof ShipppApiError) {
7
- const hint = e.status === 401 ? " — check SHIPPP_API_KEY." : "";
8
- return `Error ${e.status}: ${e.message}${hint}`;
9
- }
10
- return `Error: ${e.message}`;
11
- }
12
- function renderFiles(result) {
13
- const files = result.files.map((f) => `=== FILE: ${f.path} ===\n${f.content}`).join("\n\n");
14
- return `${files}\n\n--- RECEIPT (reproducible: ${result.receipt.reproducible}, engine: ${result.receipt.config.engine}) ---\n${JSON.stringify(result.receipt, null, 2)}`;
15
- }
16
- function renderDiff(result) {
17
- const changed = result.changed_files.map((f) => `${f.status}\t${f.path}`).join("\n");
18
- return `changed: ${result.changed}\n\n${result.diff || "(no changes)"}\n\n--- files ---\n${changed}\n\n--- RECEIPT ---\n${JSON.stringify(result.receipt, null, 2)}`;
19
- }
20
- const fileId = z.string().min(1).describe("The Shippp file id (from the file's URL, or shippp_list_units).");
21
- export function registerTools(server, client) {
22
- server.registerTool("shippp_list_units", {
23
- description: "List a Shippp file's screens and master components (id + name). Call this FIRST to discover the ids for shippp_get_component.",
24
- inputSchema: { fileId },
25
- }, async ({ fileId }) => {
26
- try {
27
- const { manifest } = await client.manifest(fileId);
28
- return text(JSON.stringify(manifest, null, 2));
29
- }
30
- catch (e) {
31
- return fail(errText(e));
32
- }
33
- });
34
- server.registerTool("shippp_get_component", {
35
- description: "Get ONE component's deterministic Chakra UI code + a reproducibility receipt. `unitType` is 'master' (a design-system component) or 'screen' (a depth-0 artboard). Get ids from shippp_list_units.",
36
- inputSchema: {
37
- fileId,
38
- unitId: z.string().min(1).describe("The master/screen id from shippp_list_units."),
39
- unitType: z.enum(["master", "screen"]),
40
- },
41
- }, async ({ fileId, unitId, unitType }) => {
42
- try {
43
- return text(renderFiles((await client.generate(fileId, { unit: "component", unitId, unitType }))));
44
- }
45
- catch (e) {
46
- return fail(errText(e));
47
- }
48
- });
49
- server.registerTool("shippp_get_tokens", {
50
- description: "Get the design system's tokens as a Chakra theme (createSystem) + receipt.",
51
- inputSchema: { fileId },
52
- }, async ({ fileId }) => {
53
- try {
54
- return text(renderFiles((await client.generate(fileId, { unit: "tokens" }))));
55
- }
56
- catch (e) {
57
- return fail(errText(e));
58
- }
59
- });
60
- server.registerTool("shippp_get_design_system", {
61
- description: "Get the whole design system as a publishable package (tokens + theme + recipes + every component + stories + package.json).",
62
- inputSchema: { fileId },
63
- }, async ({ fileId }) => {
64
- try {
65
- return text(renderFiles((await client.generate(fileId, { unit: "design-system" }))));
66
- }
67
- catch (e) {
68
- return fail(errText(e));
69
- }
70
- });
71
- server.registerTool("shippp_get_project", {
72
- description: "Get the whole file as a runnable Next.js project (screens as pages + the design system).",
73
- inputSchema: { fileId },
74
- }, async ({ fileId }) => {
75
- try {
76
- return text(renderFiles((await client.generate(fileId, { unit: "project" }))));
77
- }
78
- catch (e) {
79
- return fail(errText(e));
80
- }
81
- });
82
- server.registerTool("shippp_diff", {
83
- description: "Diff your CURRENT code against a fresh deterministic generation of a unit — shows exactly what a designer changed. Pass the same unit params as the get tools, plus `baseline` (the files you currently have for that unit).",
84
- inputSchema: {
85
- fileId,
86
- unit: z.enum(["tokens", "design-system", "project", "component"]),
87
- unitId: z.string().optional().describe("Required when unit='component'."),
88
- unitType: z.enum(["master", "screen"]).optional().describe("Required when unit='component'."),
89
- baseline: z
90
- .array(z.object({ path: z.string(), content: z.string() }))
91
- .describe("Your current files for this unit (path + content)."),
92
- },
93
- }, async ({ fileId, unit, unitId, unitType, baseline }) => {
94
- try {
95
- let request;
96
- if (unit === "component") {
97
- if (!unitId || !unitType)
98
- return fail("unit='component' requires unitId + unitType.");
99
- request = { unit, unitId, unitType };
100
- }
101
- else {
102
- request = { unit };
103
- }
104
- return text(renderDiff((await client.generate(fileId, request, baseline))));
105
- }
106
- catch (e) {
107
- return fail(errText(e));
108
- }
109
- });
110
- }