quick-proto-mcp 0.0.4
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 +21 -0
- package/README.md +110 -0
- package/dist/index.js +776 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Quick Proto contributors
|
|
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
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
## quick-proto-mcp — Model Context Protocol server (stdio)
|
|
2
|
+
|
|
3
|
+
**`npx quick-proto-mcp@latest`** runs a **local** MCP server so Claude Desktop / Cursor-style clients can publish Quick Proto previews with tools instead of invoking the CLI in a subprocess.
|
|
4
|
+
|
|
5
|
+
Requires **Node 20+**.
|
|
6
|
+
|
|
7
|
+
### Claude Desktop
|
|
8
|
+
|
|
9
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS; paths differ on Windows):
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"quick-proto": {
|
|
15
|
+
"command": "npx",
|
|
16
|
+
"args": ["-y", "quick-proto-mcp@latest"],
|
|
17
|
+
"env": {
|
|
18
|
+
"QP_API_KEY": "your-api-key"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Restart Claude after edits.
|
|
26
|
+
|
|
27
|
+
Alternatively, save credentials once with **`npx quick-proto-cli auth login`** so `~/.config/quick-proto/credentials.json` exists — then omit `env` unless you rely on CI-style keys only.
|
|
28
|
+
|
|
29
|
+
Self-hosted Quick Proto instances: set **`PROTO_BASE_URL`** to your app origin (**`http://` or `https://` only**) before starting MCP.
|
|
30
|
+
|
|
31
|
+
### Security
|
|
32
|
+
|
|
33
|
+
- **Trust model:** MCP runs **locally** over stdio with your API key from env or `~/.config/quick-proto/credentials.json`. Never put API keys in tool arguments.
|
|
34
|
+
- **`zipPath`:** Resolved with `realpath`, must be a **regular file** with a **ZIP local header** (`PK\x03\x04`). Allowed directories:
|
|
35
|
+
- **`process.cwd()`** or the OS temp directory, **or**
|
|
36
|
+
- Paths under comma-separated absolute dirs in **`QUICK_PROTO_UPLOAD_ROOT`** (each entry must be an existing directory).
|
|
37
|
+
Refuses obvious sensitive names (`credentials.json`, `.env*`, anything under `.ssh/`).
|
|
38
|
+
- **`publish_prototype` / `publish_prototype_base64`:** Default visibility is **`organization`**. Pass **`visibility: "public"`** only when you intend anyone-with-link access.
|
|
39
|
+
- **`list_versions`:** Returns a **reduced** payload (no org-member list or uploader emails) to limit data sent to the model.
|
|
40
|
+
- **Least privilege:** Prefer API keys scoped to the minimum org/project access needed for automation.
|
|
41
|
+
|
|
42
|
+
### Auth
|
|
43
|
+
|
|
44
|
+
Uses the same **Better Auth API key** as the CLI (**`QP_API_KEY`**, optional credentials file).
|
|
45
|
+
|
|
46
|
+
**Breaking change:** Older releases documented **`BUNDLE_API_KEY`** / **`PROTO_API_KEY`** — those names are no longer read; update MCP `env`, CI secrets, and shell exports to **`QP_API_KEY`**.
|
|
47
|
+
|
|
48
|
+
### Remote sandbox (Claude cloud, `/home/claude/...`)
|
|
49
|
+
|
|
50
|
+
`upload_version` / `publish_prototype` read **`zipPath` on the machine running the MCP Node process** — not the chat sandbox. Paths must lie under **cwd**, the **system temp directory**, or **`QUICK_PROTO_UPLOAD_ROOT`**, and must be real **ZIP** files (see Security above).
|
|
51
|
+
|
|
52
|
+
If the model **creates the zip inside a remote sandbox**, use the **`*_base64` tools** instead:
|
|
53
|
+
|
|
54
|
+
1. In the sandbox: `base64 -w0 bundle.zip` (or read bytes and base64-encode).
|
|
55
|
+
2. Call **`publish_prototype_base64`** or **`upload_version_base64`** with **`zipBase64`** (raw base64 or `data:application/zip;base64,...`).
|
|
56
|
+
|
|
57
|
+
Also available: **`upload_starter_base64`**, **`release_base64`**.
|
|
58
|
+
|
|
59
|
+
**Limits:** Base64 lives inside the MCP JSON payload; the host may cap tool argument size before you hit the platform’s 100 MB limit. Small/medium site exports work well; huge bundles need a **local** `zipPath` or another transfer path.
|
|
60
|
+
|
|
61
|
+
### Tools
|
|
62
|
+
|
|
63
|
+
| Tool | Purpose |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| **`list_orgs`** | Organizations for this key (`GET /api/cli/me`) |
|
|
66
|
+
| **`list_versions`** | Project bundle metadata + versions (emails / org members omitted) |
|
|
67
|
+
| **`upload_version`** | Upload SPA zip from **local disk** path (must pass MCP zip-path checks) |
|
|
68
|
+
| **`upload_version_base64`** | Upload SPA zip from **base64** (sandbox-friendly) |
|
|
69
|
+
| **`set_project_visibility`** | PATCH visibility (`public` for link sharing) |
|
|
70
|
+
| **`publish_prototype`** | Upload from path + set visibility (default **`organization`**; set `public` to share widely) |
|
|
71
|
+
| **`publish_prototype_base64`** | Same as `publish_prototype` but **`zipBase64`** |
|
|
72
|
+
| **`delete_version`** | DELETE one version |
|
|
73
|
+
| **`get_download_url`** | Signed download URL (~15 min) |
|
|
74
|
+
| **`upload_starter`** | Starter zip from path |
|
|
75
|
+
| **`upload_starter_base64`** | Starter zip from base64 |
|
|
76
|
+
| **`release`** | Bundle + starter zips from paths |
|
|
77
|
+
| **`release_base64`** | Bundle + starter from base64 |
|
|
78
|
+
|
|
79
|
+
### Manual check (MCP Inspector)
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm run build --workspace=quick-proto-mcp
|
|
83
|
+
npx @modelcontextprotocol/inspector@latest
|
|
84
|
+
# Transport: STDIO → command node → args: path/to/repo/packages/mcp/dist/index.js → cwd repo root if needed → env KEY
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Prefer rebuilding first so `dist/index.js` matches source.
|
|
88
|
+
|
|
89
|
+
### Publishing to npm
|
|
90
|
+
|
|
91
|
+
From repo root **after bumping versions** (publish **`quick-proto-client`** before **`quick-proto-cli`** and **`quick-proto-mcp`**, since both declare it as a dependency). **`prepack`** runs **`npm run build`** for each workspace before pack/publish.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm install
|
|
95
|
+
npm run test --workspace=quick-proto-client
|
|
96
|
+
npm run test --workspace=quick-proto-cli
|
|
97
|
+
npm run test --workspace=quick-proto-mcp
|
|
98
|
+
npm publish --workspace=quick-proto-client --access public
|
|
99
|
+
npm publish --workspace=quick-proto-cli --access public
|
|
100
|
+
npm publish --workspace=quick-proto-mcp --access public
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Dry run: append **`--dry-run`**.
|
|
104
|
+
|
|
105
|
+
Post-publish smoke: **`npx quick-proto-cli@latest --help`** and set **`QP_API_KEY`** when exercising **`orgs list`** or MCP **`list_orgs`**.
|
|
106
|
+
|
|
107
|
+
### Troubleshooting
|
|
108
|
+
|
|
109
|
+
- **401 / 403** — missing or stale API key; org slug must match a membership on the platform.
|
|
110
|
+
- **422 on upload** — zip missing `index.html` or malformed zip.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
|
|
6
|
+
// src/server.ts
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
import {
|
|
9
|
+
MissingApiKeyError,
|
|
10
|
+
deleteVersion as deleteRemoteVersion,
|
|
11
|
+
fetchOrgDirectory,
|
|
12
|
+
listProjectBundles,
|
|
13
|
+
mintBundleDownload,
|
|
14
|
+
patchProjectVisibility,
|
|
15
|
+
requireApiKey,
|
|
16
|
+
requireBaseUrl,
|
|
17
|
+
resolveContext,
|
|
18
|
+
uploadStarter as uploadStarterZip,
|
|
19
|
+
uploadStarterZipBuffer,
|
|
20
|
+
uploadVersion as uploadBundleVersion,
|
|
21
|
+
uploadVersionZipBuffer
|
|
22
|
+
} from "quick-proto-client";
|
|
23
|
+
|
|
24
|
+
// src/archive-name.ts
|
|
25
|
+
import path from "path";
|
|
26
|
+
function sanitizeArchiveFileName(input, fallback) {
|
|
27
|
+
const raw = (input?.trim() || fallback).replace(/\\/g, "/");
|
|
28
|
+
const base = path.basename(raw);
|
|
29
|
+
if (!base || base === "." || base === "..") return fallback;
|
|
30
|
+
return base.length > 200 ? base.slice(0, 200) : base;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/instructions.ts
|
|
34
|
+
var SERVER_INSTRUCTIONS = `
|
|
35
|
+
Quick Proto MCP publishes static SPA builds (React/Vite/Next static export etc.) so reviewers get HTTPS preview URLs on quick-proto.co.uk.
|
|
36
|
+
|
|
37
|
+
AUTH: Requires a Better Auth API key. Use QP_API_KEY, ~/.config/quick-proto/credentials.json from \`npx quick-proto-cli auth login\`, or create a key in the dashboard \u2192 API keys.
|
|
38
|
+
|
|
39
|
+
WORKFLOW:
|
|
40
|
+
1. Call list_orgs if you do not know the org slug.
|
|
41
|
+
2. Zip the built site from *inside* the output folder so index.html is at zip root:
|
|
42
|
+
\`(cd dist && zip -r ../bundle.zip .)\`
|
|
43
|
+
3a. If MCP runs on YOUR machine and the zip exists there: use publish_prototype / upload_version with zipPath (path must be under cwd, system temp, or QUICK_PROTO_UPLOAD_ROOT \u2014 only real ZIP files).
|
|
44
|
+
3b. If you built the zip in a REMOTE sandbox (e.g. /home/claude/...): read file as bytes, base64-encode, call publish_prototype_base64 or upload_version_base64 \u2014 do NOT pass sandbox paths to zipPath.
|
|
45
|
+
4. Version slug MUST NOT be the literal string "latest" \u2014 that alias is rolling only.
|
|
46
|
+
5. publish_prototype / publish_prototype_base64 default visibility is organization. Pass visibility public only when you intentionally want anyone-with-link access.
|
|
47
|
+
|
|
48
|
+
RULES:
|
|
49
|
+
- Zip must contain index.html (root or exactly one top-level folder).
|
|
50
|
+
- Max upload size matches platform limits; malformed zips \u2192 422.
|
|
51
|
+
- Base64 tools are for small/medium zips; Claude may limit tool argument size \u2014 very large builds need a local zipPath or another transfer path.
|
|
52
|
+
`.trim();
|
|
53
|
+
var MISSING_API_KEY_MESSAGE = `No API key configured.
|
|
54
|
+
|
|
55
|
+
Create one at https://quick-proto.co.uk (dashboard \u2192 API keys), then either:
|
|
56
|
+
\u2022 Set environment variable QP_API_KEY before starting this MCP server, or
|
|
57
|
+
\u2022 Run \`printf '%s\\n' '{"apiKey":"<key>"}' | npx quick-proto-cli auth login --stdin\` so credentials save to ~/.config/quick-proto/credentials.json`;
|
|
58
|
+
|
|
59
|
+
// src/project-bundle-slim.ts
|
|
60
|
+
function slimProjectBundleForMcp(p) {
|
|
61
|
+
return {
|
|
62
|
+
project: p.project,
|
|
63
|
+
org: p.org,
|
|
64
|
+
visibility: p.visibility,
|
|
65
|
+
latestUrl: p.latestUrl,
|
|
66
|
+
versions: p.versions.map((v) => ({
|
|
67
|
+
id: v.id,
|
|
68
|
+
slug: v.slug,
|
|
69
|
+
sizeBytes: v.sizeBytes,
|
|
70
|
+
isLatest: v.isLatest,
|
|
71
|
+
createdAt: v.createdAt,
|
|
72
|
+
uploadedBy: { id: v.uploadedBy.id, name: v.uploadedBy.name },
|
|
73
|
+
url: v.url
|
|
74
|
+
})),
|
|
75
|
+
starters: p.starters.map((s) => ({
|
|
76
|
+
id: s.id,
|
|
77
|
+
slug: s.slug,
|
|
78
|
+
sizeBytes: s.sizeBytes,
|
|
79
|
+
createdAt: s.createdAt,
|
|
80
|
+
uploadedBy: { id: s.uploadedBy.id, name: s.uploadedBy.name }
|
|
81
|
+
}))
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/schemas.ts
|
|
86
|
+
import { z } from "zod";
|
|
87
|
+
var PREVIEW_SLUG_SEGMENT = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
88
|
+
var MAX_SLUG_LEN = 40;
|
|
89
|
+
var slugMessage = "Lowercase alphanumeric, hyphen-separated runs only (no leading hyphen, no `--`).";
|
|
90
|
+
var visibilitySchema = z.enum([
|
|
91
|
+
"public",
|
|
92
|
+
"organization",
|
|
93
|
+
"invite_only",
|
|
94
|
+
"private"
|
|
95
|
+
]);
|
|
96
|
+
var orgSchema = z.string().trim().min(1).max(MAX_SLUG_LEN).regex(PREVIEW_SLUG_SEGMENT, slugMessage).describe("Organization slug.");
|
|
97
|
+
var projectSchema = z.string().trim().min(1).max(MAX_SLUG_LEN).regex(PREVIEW_SLUG_SEGMENT, slugMessage).describe("Project slug.");
|
|
98
|
+
var versionSchema = z.string().trim().min(1).max(MAX_SLUG_LEN).regex(PREVIEW_SLUG_SEGMENT, slugMessage).refine((v) => v !== "latest", {
|
|
99
|
+
message: 'Version slug must not be the literal "latest" (that slug is the rolling alias only).'
|
|
100
|
+
}).describe("Bundle version slug.");
|
|
101
|
+
var starterSlugSchema = z.string().trim().min(1).max(MAX_SLUG_LEN).regex(PREVIEW_SLUG_SEGMENT, slugMessage).describe("Starter template slug.");
|
|
102
|
+
var zipPathSchema = z.string().trim().min(1).describe(
|
|
103
|
+
"Path to a .zip under cwd, tmpdir, or QUICK_PROTO_UPLOAD_ROOT (must be a real ZIP file)."
|
|
104
|
+
);
|
|
105
|
+
var zipBase64Schema = z.string().trim().min(1);
|
|
106
|
+
var archiveFileNameSchema = z.string().trim().min(1).max(200).optional().describe('Filename for multipart upload (default "bundle.zip" / "starter.zip").');
|
|
107
|
+
var optionalLatestSchema = z.boolean().optional().describe(
|
|
108
|
+
'If true, mark this uploaded version as the rolling "latest" alias (defaults to true when omitted).'
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// src/tool-result.ts
|
|
112
|
+
import { FetchHttpError } from "quick-proto-client";
|
|
113
|
+
var HTTP_BODY_SNIPPET_MAX = 300;
|
|
114
|
+
function jsonText(data) {
|
|
115
|
+
return {
|
|
116
|
+
content: [
|
|
117
|
+
{
|
|
118
|
+
type: "text",
|
|
119
|
+
text: JSON.stringify(data, null, 2)
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function truncateHttpBodySnippet(body) {
|
|
125
|
+
const t = body.trim();
|
|
126
|
+
if (!t) return "";
|
|
127
|
+
let snippet = t.slice(0, HTTP_BODY_SNIPPET_MAX);
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(t);
|
|
130
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
131
|
+
const redacted = { ...parsed };
|
|
132
|
+
for (const key of Object.keys(redacted)) {
|
|
133
|
+
if (/stack|hint|detail|internal/i.test(key)) {
|
|
134
|
+
delete redacted[key];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
snippet = JSON.stringify(redacted).slice(0, HTTP_BODY_SNIPPET_MAX);
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
return snippet.length < t.length ? `${snippet}\u2026` : snippet;
|
|
142
|
+
}
|
|
143
|
+
function toolErrorMessage(err) {
|
|
144
|
+
const base = err instanceof Error ? err.message : String(err);
|
|
145
|
+
if (err instanceof FetchHttpError && err.body.trim() && err.body !== base) {
|
|
146
|
+
const snippet = truncateHttpBodySnippet(err.body);
|
|
147
|
+
return snippet ? `${base}
|
|
148
|
+
HTTP body: ${snippet}` : base;
|
|
149
|
+
}
|
|
150
|
+
return base;
|
|
151
|
+
}
|
|
152
|
+
function toolFail(message) {
|
|
153
|
+
return {
|
|
154
|
+
content: [{ type: "text", text: message }],
|
|
155
|
+
isError: true
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/zip-path.ts
|
|
160
|
+
import { constants as fsConsts } from "fs";
|
|
161
|
+
import { access, open, realpath, stat } from "fs/promises";
|
|
162
|
+
import { tmpdir } from "os";
|
|
163
|
+
import path2 from "path";
|
|
164
|
+
var ZIP_LOCAL_HEADER_MAGIC = Buffer.from([80, 75, 3, 4]);
|
|
165
|
+
function isUnderRoot(rootReal, fileReal) {
|
|
166
|
+
const normalizedRoot = path2.normalize(rootReal);
|
|
167
|
+
const normalizedFile = path2.normalize(fileReal);
|
|
168
|
+
if (normalizedFile === normalizedRoot) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
const prefix = normalizedRoot.endsWith(path2.sep) ? normalizedRoot : normalizedRoot + path2.sep;
|
|
172
|
+
return normalizedFile.startsWith(prefix);
|
|
173
|
+
}
|
|
174
|
+
function assertNotSensitiveUploadPath(canonicalPath) {
|
|
175
|
+
const base = path2.basename(canonicalPath);
|
|
176
|
+
const lowerBase = base.toLowerCase();
|
|
177
|
+
if (base === "credentials.json") {
|
|
178
|
+
throw new Error(
|
|
179
|
+
"Refusing to read credentials.json \u2014 use a build zip under your project output, not Quick Proto credentials."
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (lowerBase === ".env" || lowerBase.startsWith(".env.")) {
|
|
183
|
+
throw new Error("Refusing to upload environment files (.env*).");
|
|
184
|
+
}
|
|
185
|
+
const sep = path2.sep;
|
|
186
|
+
const norm = canonicalPath.toLowerCase();
|
|
187
|
+
const sshMarker = `${sep}.ssh${sep}`;
|
|
188
|
+
if (norm.includes(sshMarker) || norm.endsWith(`${sep}.ssh`)) {
|
|
189
|
+
throw new Error("Refusing to read paths under .ssh.");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function readZipMagicOk(filePath) {
|
|
193
|
+
const fh = await open(filePath, "r");
|
|
194
|
+
try {
|
|
195
|
+
const buf = Buffer.alloc(4);
|
|
196
|
+
const { bytesRead } = await fh.read(buf, 0, 4, 0);
|
|
197
|
+
if (bytesRead < 4 || buf[0] !== ZIP_LOCAL_HEADER_MAGIC[0] || buf[1] !== ZIP_LOCAL_HEADER_MAGIC[1] || buf[2] !== ZIP_LOCAL_HEADER_MAGIC[2] || buf[3] !== ZIP_LOCAL_HEADER_MAGIC[3]) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
"File does not look like a ZIP archive (missing PK\\x03\\x04 local header)."
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
} finally {
|
|
203
|
+
await fh.close().catch(() => {
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function resolveAllowedRoots() {
|
|
208
|
+
const raw = process.env.QUICK_PROTO_UPLOAD_ROOT?.trim();
|
|
209
|
+
if (raw) {
|
|
210
|
+
const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
211
|
+
const roots = [];
|
|
212
|
+
for (const p of parts) {
|
|
213
|
+
if (!path2.isAbsolute(p)) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`QUICK_PROTO_UPLOAD_ROOT entries must be absolute paths; got "${p}".`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
let rp;
|
|
219
|
+
try {
|
|
220
|
+
rp = await realpath(p);
|
|
221
|
+
} catch {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`QUICK_PROTO_UPLOAD_ROOT path not found or inaccessible: "${p}".`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
const st = await stat(rp);
|
|
227
|
+
if (!st.isDirectory()) {
|
|
228
|
+
throw new Error(`QUICK_PROTO_UPLOAD_ROOT must be a directory: "${p}".`);
|
|
229
|
+
}
|
|
230
|
+
roots.push(rp);
|
|
231
|
+
}
|
|
232
|
+
return [...new Set(roots)];
|
|
233
|
+
}
|
|
234
|
+
const cwd = await realpath(process.cwd());
|
|
235
|
+
let tmp;
|
|
236
|
+
try {
|
|
237
|
+
tmp = await realpath(tmpdir());
|
|
238
|
+
} catch {
|
|
239
|
+
tmp = path2.normalize(tmpdir());
|
|
240
|
+
}
|
|
241
|
+
return cwd === tmp ? [cwd] : [cwd, tmp];
|
|
242
|
+
}
|
|
243
|
+
async function resolveVerifiedZipPath(userPath) {
|
|
244
|
+
const trimmed = userPath.trim();
|
|
245
|
+
if (!trimmed) {
|
|
246
|
+
throw new Error("ZIP path is empty.");
|
|
247
|
+
}
|
|
248
|
+
const resolved = path2.resolve(process.cwd(), trimmed);
|
|
249
|
+
let canonical;
|
|
250
|
+
try {
|
|
251
|
+
canonical = await realpath(resolved);
|
|
252
|
+
} catch {
|
|
253
|
+
throw new Error(`ZIP path not found or inaccessible: ${trimmed}`);
|
|
254
|
+
}
|
|
255
|
+
const st = await stat(canonical);
|
|
256
|
+
if (!st.isFile()) {
|
|
257
|
+
throw new Error(`ZIP path must be a regular file: ${canonical}`);
|
|
258
|
+
}
|
|
259
|
+
await access(canonical, fsConsts.R_OK);
|
|
260
|
+
assertNotSensitiveUploadPath(canonical);
|
|
261
|
+
const roots = await resolveAllowedRoots();
|
|
262
|
+
const allowed = roots.some((root) => isUnderRoot(root, canonical));
|
|
263
|
+
if (!allowed) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
`ZIP path is outside allowed directories (${roots.join(", ")}). Set QUICK_PROTO_UPLOAD_ROOT to extend. Resolved: ${canonical}`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
await readZipMagicOk(canonical);
|
|
269
|
+
return canonical;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/zip-base64.ts
|
|
273
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
274
|
+
import { MAX_ZIP_UPLOAD_BYTES } from "quick-proto-client";
|
|
275
|
+
var MAX_ZIP_BASE64_PAYLOAD_CHARS = Math.ceil(MAX_ZIP_UPLOAD_BYTES * 4 / 3) + 128;
|
|
276
|
+
function decodeZipBase64(input) {
|
|
277
|
+
const trimmed = input.trim();
|
|
278
|
+
if (!trimmed) {
|
|
279
|
+
throw new Error("zipBase64 is empty.");
|
|
280
|
+
}
|
|
281
|
+
const payload = /^data:[^;]+;base64,/i.test(trimmed) ? trimmed.replace(/^[^,]+,/, "") : trimmed.replace(/\s+/g, "");
|
|
282
|
+
if (payload.length > MAX_ZIP_BASE64_PAYLOAD_CHARS) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`zipBase64 exceeds maximum encoded length (${MAX_ZIP_BASE64_PAYLOAD_CHARS} characters).`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
const buf = Buffer2.from(payload, "base64");
|
|
288
|
+
if (buf.byteLength === 0) {
|
|
289
|
+
throw new Error("Decoded base64 is empty \u2014 check zipBase64 is valid ZIP base64.");
|
|
290
|
+
}
|
|
291
|
+
if (!(buf[0] === 80 && buf[1] === 75)) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
"Decoded bytes do not look like a zip file (missing PK header). Wrong encoding or truncation."
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
return buf;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/server.ts
|
|
300
|
+
async function requireAuthCtx() {
|
|
301
|
+
const ctx = await resolveContext({});
|
|
302
|
+
requireBaseUrl(ctx);
|
|
303
|
+
requireApiKey(ctx);
|
|
304
|
+
return ctx;
|
|
305
|
+
}
|
|
306
|
+
async function withAuth(run) {
|
|
307
|
+
try {
|
|
308
|
+
const ctx = await requireAuthCtx();
|
|
309
|
+
return await run(ctx);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
if (err instanceof MissingApiKeyError) {
|
|
312
|
+
return toolFail(MISSING_API_KEY_MESSAGE);
|
|
313
|
+
}
|
|
314
|
+
return toolFail(toolErrorMessage(err));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function createQuickProtoMcp() {
|
|
318
|
+
const mcpServer = new McpServer(
|
|
319
|
+
{ name: "quick-proto", version: "0.0.3" },
|
|
320
|
+
{ instructions: SERVER_INSTRUCTIONS }
|
|
321
|
+
);
|
|
322
|
+
mcpServer.registerTool(
|
|
323
|
+
"list_orgs",
|
|
324
|
+
{
|
|
325
|
+
description: "List organizations available to this API key (GET /api/cli/me). Call first when org slug is unknown."
|
|
326
|
+
},
|
|
327
|
+
async () => withAuth(async (ctx) => {
|
|
328
|
+
try {
|
|
329
|
+
const payload = await fetchOrgDirectory(ctx);
|
|
330
|
+
return jsonText(payload);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
return toolFail(toolErrorMessage(err));
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
);
|
|
336
|
+
mcpServer.registerTool(
|
|
337
|
+
"list_versions",
|
|
338
|
+
{
|
|
339
|
+
description: "List bundle versions for a project, visibility, starters, preview URLs (GET /api/bundles/:org/:project). Omits org-member emails from the payload.",
|
|
340
|
+
inputSchema: {
|
|
341
|
+
org: orgSchema,
|
|
342
|
+
project: projectSchema
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
async (args) => withAuth(async (ctx) => {
|
|
346
|
+
try {
|
|
347
|
+
const payload = await listProjectBundles({
|
|
348
|
+
ctx,
|
|
349
|
+
org: args.org,
|
|
350
|
+
project: args.project
|
|
351
|
+
});
|
|
352
|
+
return jsonText(slimProjectBundleForMcp(payload));
|
|
353
|
+
} catch (err) {
|
|
354
|
+
return toolFail(toolErrorMessage(err));
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
);
|
|
358
|
+
mcpServer.registerTool(
|
|
359
|
+
"upload_version",
|
|
360
|
+
{
|
|
361
|
+
description: "Upload a zip as a new preview bundle version. Zip must contain index.html at zip root.",
|
|
362
|
+
inputSchema: {
|
|
363
|
+
org: orgSchema,
|
|
364
|
+
project: projectSchema,
|
|
365
|
+
version: versionSchema.describe(
|
|
366
|
+
'Version label (never the literal slug "latest").'
|
|
367
|
+
),
|
|
368
|
+
zipPath: zipPathSchema,
|
|
369
|
+
latest: optionalLatestSchema
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
async (args) => withAuth(async (ctx) => {
|
|
373
|
+
try {
|
|
374
|
+
const zipPath = await resolveVerifiedZipPath(args.zipPath);
|
|
375
|
+
const latest = args.latest ?? true;
|
|
376
|
+
const payload = await uploadBundleVersion({
|
|
377
|
+
ctx,
|
|
378
|
+
org: args.org,
|
|
379
|
+
project: args.project,
|
|
380
|
+
version: args.version,
|
|
381
|
+
zipPath,
|
|
382
|
+
latest
|
|
383
|
+
});
|
|
384
|
+
return jsonText(payload);
|
|
385
|
+
} catch (err) {
|
|
386
|
+
return toolFail(toolErrorMessage(err));
|
|
387
|
+
}
|
|
388
|
+
})
|
|
389
|
+
);
|
|
390
|
+
mcpServer.registerTool(
|
|
391
|
+
"upload_version_base64",
|
|
392
|
+
{
|
|
393
|
+
description: "Same as upload_version but zip contents are passed as base64 \u2014 use when Claude built the archive in a remote sandbox (/home/claude/...) where MCP disk paths differ. MCP/chat may truncate very large payloads; prefer zipPath locally for huge bundles.",
|
|
394
|
+
inputSchema: {
|
|
395
|
+
org: orgSchema,
|
|
396
|
+
project: projectSchema,
|
|
397
|
+
version: versionSchema.describe(
|
|
398
|
+
'Version label (never the literal slug "latest").'
|
|
399
|
+
),
|
|
400
|
+
zipBase64: zipBase64Schema.describe(
|
|
401
|
+
"Base64-encoded .zip bytes, or data:application/zip;base64,..."
|
|
402
|
+
),
|
|
403
|
+
archiveFileName: archiveFileNameSchema,
|
|
404
|
+
latest: optionalLatestSchema
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
async (args) => withAuth(async (ctx) => {
|
|
408
|
+
try {
|
|
409
|
+
const buf = decodeZipBase64(args.zipBase64);
|
|
410
|
+
const latest = args.latest ?? true;
|
|
411
|
+
const name = sanitizeArchiveFileName(args.archiveFileName, "bundle.zip");
|
|
412
|
+
const payload = await uploadVersionZipBuffer({
|
|
413
|
+
ctx,
|
|
414
|
+
org: args.org,
|
|
415
|
+
project: args.project,
|
|
416
|
+
version: args.version,
|
|
417
|
+
zipBuffer: buf,
|
|
418
|
+
archiveFileName: name,
|
|
419
|
+
latest
|
|
420
|
+
});
|
|
421
|
+
return jsonText(payload);
|
|
422
|
+
} catch (err) {
|
|
423
|
+
return toolFail(toolErrorMessage(err));
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
);
|
|
427
|
+
mcpServer.registerTool(
|
|
428
|
+
"set_project_visibility",
|
|
429
|
+
{
|
|
430
|
+
description: "PATCH preview visibility: public allows anyone-with-link access.",
|
|
431
|
+
inputSchema: {
|
|
432
|
+
org: orgSchema,
|
|
433
|
+
project: projectSchema,
|
|
434
|
+
visibility: visibilitySchema
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
async (args) => withAuth(async (ctx) => {
|
|
438
|
+
try {
|
|
439
|
+
const payload = await patchProjectVisibility({
|
|
440
|
+
ctx,
|
|
441
|
+
org: args.org,
|
|
442
|
+
project: args.project,
|
|
443
|
+
visibility: args.visibility
|
|
444
|
+
});
|
|
445
|
+
return jsonText(payload);
|
|
446
|
+
} catch (err) {
|
|
447
|
+
return toolFail(toolErrorMessage(err));
|
|
448
|
+
}
|
|
449
|
+
})
|
|
450
|
+
);
|
|
451
|
+
mcpServer.registerTool(
|
|
452
|
+
"publish_prototype",
|
|
453
|
+
{
|
|
454
|
+
description: "Composite: uploads a bundle zip then sets project visibility (defaults to organization; pass public for anyone-with-link sharing).",
|
|
455
|
+
inputSchema: {
|
|
456
|
+
org: orgSchema,
|
|
457
|
+
project: projectSchema,
|
|
458
|
+
version: versionSchema.describe(
|
|
459
|
+
'Version label (never the literal slug "latest").'
|
|
460
|
+
),
|
|
461
|
+
zipPath: zipPathSchema,
|
|
462
|
+
latest: optionalLatestSchema,
|
|
463
|
+
visibility: visibilitySchema.optional().describe(
|
|
464
|
+
'Visibility after upload. Default "organization"; use "public" only when intentional link sharing.'
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
async (args) => withAuth(async (ctx) => {
|
|
469
|
+
try {
|
|
470
|
+
const zipPath = await resolveVerifiedZipPath(args.zipPath);
|
|
471
|
+
const latest = args.latest ?? true;
|
|
472
|
+
const visibility = args.visibility ?? "organization";
|
|
473
|
+
const uploaded = await uploadBundleVersion({
|
|
474
|
+
ctx,
|
|
475
|
+
org: args.org,
|
|
476
|
+
project: args.project,
|
|
477
|
+
version: args.version,
|
|
478
|
+
zipPath,
|
|
479
|
+
latest
|
|
480
|
+
});
|
|
481
|
+
const updated = await patchProjectVisibility({
|
|
482
|
+
ctx,
|
|
483
|
+
org: args.org,
|
|
484
|
+
project: args.project,
|
|
485
|
+
visibility
|
|
486
|
+
});
|
|
487
|
+
return jsonText({
|
|
488
|
+
upload: uploaded,
|
|
489
|
+
projectPatch: updated,
|
|
490
|
+
versionUrl: uploaded.urls.version,
|
|
491
|
+
latestUrl: uploaded.urls.latest,
|
|
492
|
+
visibility
|
|
493
|
+
});
|
|
494
|
+
} catch (err) {
|
|
495
|
+
return toolFail(toolErrorMessage(err));
|
|
496
|
+
}
|
|
497
|
+
})
|
|
498
|
+
);
|
|
499
|
+
mcpServer.registerTool(
|
|
500
|
+
"publish_prototype_base64",
|
|
501
|
+
{
|
|
502
|
+
description: "Like publish_prototype but ZIP is zipBase64 (sandbox-friendly). Prefer when there is no shared filesystem between chat and MCP host.",
|
|
503
|
+
inputSchema: {
|
|
504
|
+
org: orgSchema,
|
|
505
|
+
project: projectSchema,
|
|
506
|
+
version: versionSchema.describe(
|
|
507
|
+
'Version label (never the literal slug "latest").'
|
|
508
|
+
),
|
|
509
|
+
zipBase64: zipBase64Schema,
|
|
510
|
+
archiveFileName: archiveFileNameSchema,
|
|
511
|
+
latest: optionalLatestSchema,
|
|
512
|
+
visibility: visibilitySchema.optional().describe(
|
|
513
|
+
'Visibility after upload. Default "organization"; use "public" only when intentional link sharing.'
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
async (args) => withAuth(async (ctx) => {
|
|
518
|
+
try {
|
|
519
|
+
const buf = decodeZipBase64(args.zipBase64);
|
|
520
|
+
const latest = args.latest ?? true;
|
|
521
|
+
const visibility = args.visibility ?? "organization";
|
|
522
|
+
const archiveName = sanitizeArchiveFileName(args.archiveFileName, "bundle.zip");
|
|
523
|
+
const uploaded = await uploadVersionZipBuffer({
|
|
524
|
+
ctx,
|
|
525
|
+
org: args.org,
|
|
526
|
+
project: args.project,
|
|
527
|
+
version: args.version,
|
|
528
|
+
zipBuffer: buf,
|
|
529
|
+
archiveFileName: archiveName,
|
|
530
|
+
latest
|
|
531
|
+
});
|
|
532
|
+
const updated = await patchProjectVisibility({
|
|
533
|
+
ctx,
|
|
534
|
+
org: args.org,
|
|
535
|
+
project: args.project,
|
|
536
|
+
visibility
|
|
537
|
+
});
|
|
538
|
+
return jsonText({
|
|
539
|
+
upload: uploaded,
|
|
540
|
+
projectPatch: updated,
|
|
541
|
+
versionUrl: uploaded.urls.version,
|
|
542
|
+
latestUrl: uploaded.urls.latest,
|
|
543
|
+
visibility
|
|
544
|
+
});
|
|
545
|
+
} catch (err) {
|
|
546
|
+
return toolFail(toolErrorMessage(err));
|
|
547
|
+
}
|
|
548
|
+
})
|
|
549
|
+
);
|
|
550
|
+
mcpServer.registerTool(
|
|
551
|
+
"delete_version",
|
|
552
|
+
{
|
|
553
|
+
description: "Delete one bundle version from R2/registry (not the literal latest alias slug).",
|
|
554
|
+
inputSchema: {
|
|
555
|
+
org: orgSchema,
|
|
556
|
+
project: projectSchema,
|
|
557
|
+
version: versionSchema.describe(
|
|
558
|
+
"Concrete version slug to remove."
|
|
559
|
+
)
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
async (args) => withAuth(async (ctx) => {
|
|
563
|
+
try {
|
|
564
|
+
await deleteRemoteVersion({
|
|
565
|
+
ctx,
|
|
566
|
+
org: args.org,
|
|
567
|
+
project: args.project,
|
|
568
|
+
version: args.version
|
|
569
|
+
});
|
|
570
|
+
return jsonText({ ok: true, deleted: args.version });
|
|
571
|
+
} catch (err) {
|
|
572
|
+
return toolFail(toolErrorMessage(err));
|
|
573
|
+
}
|
|
574
|
+
})
|
|
575
|
+
);
|
|
576
|
+
mcpServer.registerTool(
|
|
577
|
+
"get_download_url",
|
|
578
|
+
{
|
|
579
|
+
description: "Mint a signed same-origin bundle download URL (valid ~15 minutes).",
|
|
580
|
+
inputSchema: {
|
|
581
|
+
org: orgSchema,
|
|
582
|
+
project: projectSchema,
|
|
583
|
+
version: versionSchema
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
async (args) => withAuth(async (ctx) => {
|
|
587
|
+
try {
|
|
588
|
+
const meta = await mintBundleDownload({
|
|
589
|
+
ctx,
|
|
590
|
+
org: args.org,
|
|
591
|
+
project: args.project,
|
|
592
|
+
version: args.version
|
|
593
|
+
});
|
|
594
|
+
return jsonText(meta);
|
|
595
|
+
} catch (err) {
|
|
596
|
+
return toolFail(toolErrorMessage(err));
|
|
597
|
+
}
|
|
598
|
+
})
|
|
599
|
+
);
|
|
600
|
+
mcpServer.registerTool(
|
|
601
|
+
"upload_starter",
|
|
602
|
+
{
|
|
603
|
+
description: "Upload a starter/scaffold zip for the project (any zip layout permitted).",
|
|
604
|
+
inputSchema: {
|
|
605
|
+
org: orgSchema,
|
|
606
|
+
project: projectSchema,
|
|
607
|
+
slug: starterSlugSchema,
|
|
608
|
+
zipPath: zipPathSchema
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
async (args) => withAuth(async (ctx) => {
|
|
612
|
+
try {
|
|
613
|
+
const zipPath = await resolveVerifiedZipPath(args.zipPath);
|
|
614
|
+
const payload = await uploadStarterZip({
|
|
615
|
+
ctx,
|
|
616
|
+
org: args.org,
|
|
617
|
+
project: args.project,
|
|
618
|
+
slug: args.slug,
|
|
619
|
+
zipPath
|
|
620
|
+
});
|
|
621
|
+
return jsonText(payload);
|
|
622
|
+
} catch (err) {
|
|
623
|
+
return toolFail(toolErrorMessage(err));
|
|
624
|
+
}
|
|
625
|
+
})
|
|
626
|
+
);
|
|
627
|
+
mcpServer.registerTool(
|
|
628
|
+
"upload_starter_base64",
|
|
629
|
+
{
|
|
630
|
+
description: "Same as upload_starter but ZIP is zipBase64 (for remote sandboxes).",
|
|
631
|
+
inputSchema: {
|
|
632
|
+
org: orgSchema,
|
|
633
|
+
project: projectSchema,
|
|
634
|
+
slug: starterSlugSchema,
|
|
635
|
+
zipBase64: zipBase64Schema,
|
|
636
|
+
archiveFileName: archiveFileNameSchema
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
async (args) => withAuth(async (ctx) => {
|
|
640
|
+
try {
|
|
641
|
+
const buf = decodeZipBase64(args.zipBase64);
|
|
642
|
+
const name = sanitizeArchiveFileName(args.archiveFileName, "starter.zip");
|
|
643
|
+
const payload = await uploadStarterZipBuffer({
|
|
644
|
+
ctx,
|
|
645
|
+
org: args.org,
|
|
646
|
+
project: args.project,
|
|
647
|
+
slug: args.slug,
|
|
648
|
+
zipBuffer: buf,
|
|
649
|
+
archiveFileName: name
|
|
650
|
+
});
|
|
651
|
+
return jsonText(payload);
|
|
652
|
+
} catch (err) {
|
|
653
|
+
return toolFail(toolErrorMessage(err));
|
|
654
|
+
}
|
|
655
|
+
})
|
|
656
|
+
);
|
|
657
|
+
mcpServer.registerTool(
|
|
658
|
+
"release",
|
|
659
|
+
{
|
|
660
|
+
description: "Upload preview bundle zip then starter zip (recommended order \u2014 matches CLI release).",
|
|
661
|
+
inputSchema: {
|
|
662
|
+
org: orgSchema,
|
|
663
|
+
project: projectSchema,
|
|
664
|
+
version: versionSchema.describe(
|
|
665
|
+
'Preview bundle version (never the literal slug "latest").'
|
|
666
|
+
),
|
|
667
|
+
bundleZipPath: zipPathSchema.describe("Path to preview/site zip."),
|
|
668
|
+
starterZipPath: zipPathSchema.describe("Path to starter/template zip."),
|
|
669
|
+
starterSlug: starterSlugSchema.optional().describe('Starter slug; default "source".'),
|
|
670
|
+
latest: optionalLatestSchema
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
async (args) => withAuth(async (ctx) => {
|
|
674
|
+
try {
|
|
675
|
+
const bundleZipPath = await resolveVerifiedZipPath(args.bundleZipPath);
|
|
676
|
+
const starterZipPath = await resolveVerifiedZipPath(args.starterZipPath);
|
|
677
|
+
const latest = args.latest ?? true;
|
|
678
|
+
const slug = args.starterSlug ?? "source";
|
|
679
|
+
const bundle = await uploadBundleVersion({
|
|
680
|
+
ctx,
|
|
681
|
+
org: args.org,
|
|
682
|
+
project: args.project,
|
|
683
|
+
version: args.version,
|
|
684
|
+
zipPath: bundleZipPath,
|
|
685
|
+
latest
|
|
686
|
+
});
|
|
687
|
+
const starter = await uploadStarterZip({
|
|
688
|
+
ctx,
|
|
689
|
+
org: args.org,
|
|
690
|
+
project: args.project,
|
|
691
|
+
slug,
|
|
692
|
+
zipPath: starterZipPath
|
|
693
|
+
});
|
|
694
|
+
return jsonText({ bundle, starter });
|
|
695
|
+
} catch (err) {
|
|
696
|
+
return toolFail(toolErrorMessage(err));
|
|
697
|
+
}
|
|
698
|
+
})
|
|
699
|
+
);
|
|
700
|
+
mcpServer.registerTool(
|
|
701
|
+
"release_base64",
|
|
702
|
+
{
|
|
703
|
+
description: "Like release but both zips as base64 (bundle then starter \u2014 sandbox-safe). Same payload-size caveats as other *_base64 tools.",
|
|
704
|
+
inputSchema: {
|
|
705
|
+
org: orgSchema,
|
|
706
|
+
project: projectSchema,
|
|
707
|
+
version: versionSchema.describe(
|
|
708
|
+
'Preview bundle version (never the literal slug "latest").'
|
|
709
|
+
),
|
|
710
|
+
bundleZipBase64: zipBase64Schema.describe("Base64-encoded preview/site zip."),
|
|
711
|
+
starterZipBase64: zipBase64Schema.describe(
|
|
712
|
+
"Base64-encoded starter zip."
|
|
713
|
+
),
|
|
714
|
+
bundleArchiveFileName: archiveFileNameSchema.optional(),
|
|
715
|
+
starterArchiveFileName: archiveFileNameSchema.optional(),
|
|
716
|
+
starterSlug: starterSlugSchema.optional(),
|
|
717
|
+
latest: optionalLatestSchema
|
|
718
|
+
}
|
|
719
|
+
},
|
|
720
|
+
async (args) => withAuth(async (ctx) => {
|
|
721
|
+
try {
|
|
722
|
+
const bundleBuf = decodeZipBase64(args.bundleZipBase64);
|
|
723
|
+
const starterBuf = decodeZipBase64(args.starterZipBase64);
|
|
724
|
+
const latest = args.latest ?? true;
|
|
725
|
+
const slug = args.starterSlug ?? "source";
|
|
726
|
+
const bundleName = sanitizeArchiveFileName(
|
|
727
|
+
args.bundleArchiveFileName,
|
|
728
|
+
"bundle.zip"
|
|
729
|
+
);
|
|
730
|
+
const starterName = sanitizeArchiveFileName(
|
|
731
|
+
args.starterArchiveFileName,
|
|
732
|
+
"starter.zip"
|
|
733
|
+
);
|
|
734
|
+
const bundle = await uploadVersionZipBuffer({
|
|
735
|
+
ctx,
|
|
736
|
+
org: args.org,
|
|
737
|
+
project: args.project,
|
|
738
|
+
version: args.version,
|
|
739
|
+
zipBuffer: bundleBuf,
|
|
740
|
+
archiveFileName: bundleName,
|
|
741
|
+
latest
|
|
742
|
+
});
|
|
743
|
+
const starter = await uploadStarterZipBuffer({
|
|
744
|
+
ctx,
|
|
745
|
+
org: args.org,
|
|
746
|
+
project: args.project,
|
|
747
|
+
slug,
|
|
748
|
+
zipBuffer: starterBuf,
|
|
749
|
+
archiveFileName: starterName
|
|
750
|
+
});
|
|
751
|
+
return jsonText({ bundle, starter });
|
|
752
|
+
} catch (err) {
|
|
753
|
+
return toolFail(toolErrorMessage(err));
|
|
754
|
+
}
|
|
755
|
+
})
|
|
756
|
+
);
|
|
757
|
+
return mcpServer;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// src/index.ts
|
|
761
|
+
async function main() {
|
|
762
|
+
const mcpServer = createQuickProtoMcp();
|
|
763
|
+
const transport = new StdioServerTransport();
|
|
764
|
+
await mcpServer.connect(transport);
|
|
765
|
+
const shutdown = async () => {
|
|
766
|
+
await mcpServer.close();
|
|
767
|
+
process.exit(0);
|
|
768
|
+
};
|
|
769
|
+
process.on("SIGINT", () => void shutdown());
|
|
770
|
+
process.on("SIGTERM", () => void shutdown());
|
|
771
|
+
}
|
|
772
|
+
main().catch((err) => {
|
|
773
|
+
console.error(err instanceof Error ? err.stack ?? err.message : err);
|
|
774
|
+
process.exit(1);
|
|
775
|
+
});
|
|
776
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/server.ts","../src/archive-name.ts","../src/instructions.ts","../src/project-bundle-slim.ts","../src/schemas.ts","../src/tool-result.ts","../src/zip-path.ts","../src/zip-base64.ts"],"sourcesContent":["import { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\"\n\nimport { createQuickProtoMcp } from \"./server.js\"\n\nasync function main(): Promise<void> {\n const mcpServer = createQuickProtoMcp()\n const transport = new StdioServerTransport()\n await mcpServer.connect(transport)\n\n const shutdown = async (): Promise<void> => {\n await mcpServer.close()\n process.exit(0)\n }\n process.on(\"SIGINT\", () => void shutdown())\n process.on(\"SIGTERM\", () => void shutdown())\n}\n\nmain().catch((err: unknown) => {\n console.error(err instanceof Error ? err.stack ?? err.message : err)\n process.exit(1)\n})\n","/* eslint-disable @typescript-eslint/ban-ts-comment -- MCP SDK tool typing explodes tsc; @ts-nocheck limited to this entry */\n// @ts-nocheck — McpServer + tool callbacks pull very large inferred types from @modelcontextprotocol/sdk; other modules stay fully checked.\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\"\n\nimport {\n MissingApiKeyError,\n deleteVersion as deleteRemoteVersion,\n fetchOrgDirectory,\n listProjectBundles,\n mintBundleDownload,\n patchProjectVisibility,\n requireApiKey,\n requireBaseUrl,\n resolveContext,\n uploadStarter as uploadStarterZip,\n uploadStarterZipBuffer,\n uploadVersion as uploadBundleVersion,\n uploadVersionZipBuffer,\n type CliContext,\n type Visibility,\n} from \"quick-proto-client\"\n\nimport { sanitizeArchiveFileName } from \"./archive-name.js\"\nimport { MISSING_API_KEY_MESSAGE, SERVER_INSTRUCTIONS } from \"./instructions.js\"\nimport { slimProjectBundleForMcp } from \"./project-bundle-slim.js\"\nimport {\n archiveFileNameSchema,\n visibilitySchema,\n orgSchema,\n projectSchema,\n versionSchema,\n zipBase64Schema,\n zipPathSchema,\n starterSlugSchema,\n optionalLatestSchema,\n} from \"./schemas.js\"\nimport { jsonText, toolErrorMessage, toolFail } from \"./tool-result.js\"\nimport { resolveVerifiedZipPath } from \"./zip-path.js\"\nimport { decodeZipBase64 } from \"./zip-base64.js\"\n\ntype TextToolResult = ReturnType<typeof jsonText> | ReturnType<typeof toolFail>\n\nasync function requireAuthCtx(): Promise<CliContext & { apiKey: string }> {\n const ctx = await resolveContext({})\n requireBaseUrl(ctx)\n requireApiKey(ctx)\n return ctx\n}\n\nasync function withAuth(\n run: (ctx: CliContext & { apiKey: string }) => Promise<TextToolResult>\n): Promise<TextToolResult> {\n try {\n const ctx = await requireAuthCtx()\n return await run(ctx)\n } catch (err) {\n if (err instanceof MissingApiKeyError) {\n return toolFail(MISSING_API_KEY_MESSAGE)\n }\n return toolFail(toolErrorMessage(err))\n }\n}\n\nexport function createQuickProtoMcp(): McpServer {\n const mcpServer = new McpServer(\n { name: \"quick-proto\", version: \"0.0.3\" },\n { instructions: SERVER_INSTRUCTIONS }\n )\n\n mcpServer.registerTool(\n \"list_orgs\",\n {\n description:\n \"List organizations available to this API key (GET /api/cli/me). Call first when org slug is unknown.\",\n },\n async () =>\n withAuth(async (ctx) => {\n try {\n const payload = await fetchOrgDirectory(ctx)\n return jsonText(payload)\n } catch (err) {\n return toolFail(toolErrorMessage(err))\n }\n })\n )\n\n mcpServer.registerTool(\n \"list_versions\",\n {\n description:\n \"List bundle versions for a project, visibility, starters, preview URLs (GET /api/bundles/:org/:project). Omits org-member emails from the payload.\",\n inputSchema: {\n org: orgSchema,\n project: projectSchema,\n },\n },\n async (args: { org: string; project: string }) =>\n withAuth(async (ctx) => {\n try {\n const payload = await listProjectBundles({\n ctx,\n org: args.org,\n project: args.project,\n })\n return jsonText(slimProjectBundleForMcp(payload))\n } catch (err) {\n return toolFail(toolErrorMessage(err))\n }\n })\n )\n\n mcpServer.registerTool(\n \"upload_version\",\n {\n description:\n \"Upload a zip as a new preview bundle version. Zip must contain index.html at zip root.\",\n inputSchema: {\n org: orgSchema,\n project: projectSchema,\n version: versionSchema.describe(\n 'Version label (never the literal slug \"latest\").'\n ),\n zipPath: zipPathSchema,\n latest: optionalLatestSchema,\n },\n },\n async (args: {\n org: string\n project: string\n version: string\n zipPath: string\n latest?: boolean\n }) =>\n withAuth(async (ctx) => {\n try {\n const zipPath = await resolveVerifiedZipPath(args.zipPath)\n const latest = args.latest ?? true\n const payload = await uploadBundleVersion({\n ctx,\n org: args.org,\n project: args.project,\n version: args.version,\n zipPath,\n latest,\n })\n return jsonText(payload)\n } catch (err) {\n return toolFail(toolErrorMessage(err))\n }\n })\n )\n\n mcpServer.registerTool(\n \"upload_version_base64\",\n {\n description:\n \"Same as upload_version but zip contents are passed as base64 — use when Claude built the archive in a remote sandbox (/home/claude/...) where MCP disk paths differ. MCP/chat may truncate very large payloads; prefer zipPath locally for huge bundles.\",\n inputSchema: {\n org: orgSchema,\n project: projectSchema,\n version: versionSchema.describe(\n 'Version label (never the literal slug \"latest\").'\n ),\n zipBase64: zipBase64Schema.describe(\n \"Base64-encoded .zip bytes, or data:application/zip;base64,...\"\n ),\n archiveFileName: archiveFileNameSchema,\n latest: optionalLatestSchema,\n },\n },\n async (args: {\n org: string\n project: string\n version: string\n zipBase64: string\n archiveFileName?: string\n latest?: boolean\n }) =>\n withAuth(async (ctx) => {\n try {\n const buf = decodeZipBase64(args.zipBase64)\n const latest = args.latest ?? true\n const name = sanitizeArchiveFileName(args.archiveFileName, \"bundle.zip\")\n const payload = await uploadVersionZipBuffer({\n ctx,\n org: args.org,\n project: args.project,\n version: args.version,\n zipBuffer: buf,\n archiveFileName: name,\n latest,\n })\n return jsonText(payload)\n } catch (err) {\n return toolFail(toolErrorMessage(err))\n }\n })\n )\n\n mcpServer.registerTool(\n \"set_project_visibility\",\n {\n description: \"PATCH preview visibility: public allows anyone-with-link access.\",\n inputSchema: {\n org: orgSchema,\n project: projectSchema,\n visibility: visibilitySchema,\n },\n },\n async (args: { org: string; project: string; visibility: Visibility }) =>\n withAuth(async (ctx) => {\n try {\n const payload = await patchProjectVisibility({\n ctx,\n org: args.org,\n project: args.project,\n visibility: args.visibility,\n })\n return jsonText(payload)\n } catch (err) {\n return toolFail(toolErrorMessage(err))\n }\n })\n )\n\n mcpServer.registerTool(\n \"publish_prototype\",\n {\n description:\n \"Composite: uploads a bundle zip then sets project visibility (defaults to organization; pass public for anyone-with-link sharing).\",\n inputSchema: {\n org: orgSchema,\n project: projectSchema,\n version: versionSchema.describe(\n 'Version label (never the literal slug \"latest\").'\n ),\n zipPath: zipPathSchema,\n latest: optionalLatestSchema,\n visibility: visibilitySchema\n .optional()\n .describe(\n 'Visibility after upload. Default \"organization\"; use \"public\" only when intentional link sharing.'\n ),\n },\n },\n async (args: {\n org: string\n project: string\n version: string\n zipPath: string\n latest?: boolean\n visibility?: Visibility\n }) =>\n withAuth(async (ctx) => {\n try {\n const zipPath = await resolveVerifiedZipPath(args.zipPath)\n const latest = args.latest ?? true\n const visibility = args.visibility ?? \"organization\"\n const uploaded = await uploadBundleVersion({\n ctx,\n org: args.org,\n project: args.project,\n version: args.version,\n zipPath,\n latest,\n })\n const updated = await patchProjectVisibility({\n ctx,\n org: args.org,\n project: args.project,\n visibility,\n })\n return jsonText({\n upload: uploaded,\n projectPatch: updated,\n versionUrl: uploaded.urls.version,\n latestUrl: uploaded.urls.latest,\n visibility,\n })\n } catch (err) {\n return toolFail(toolErrorMessage(err))\n }\n })\n )\n\n mcpServer.registerTool(\n \"publish_prototype_base64\",\n {\n description:\n 'Like publish_prototype but ZIP is zipBase64 (sandbox-friendly). Prefer when there is no shared filesystem between chat and MCP host.',\n inputSchema: {\n org: orgSchema,\n project: projectSchema,\n version: versionSchema.describe(\n 'Version label (never the literal slug \"latest\").'\n ),\n zipBase64: zipBase64Schema,\n archiveFileName: archiveFileNameSchema,\n latest: optionalLatestSchema,\n visibility: visibilitySchema\n .optional()\n .describe(\n 'Visibility after upload. Default \"organization\"; use \"public\" only when intentional link sharing.'\n ),\n },\n },\n async (args: {\n org: string\n project: string\n version: string\n zipBase64: string\n archiveFileName?: string\n latest?: boolean\n visibility?: Visibility\n }) =>\n withAuth(async (ctx) => {\n try {\n const buf = decodeZipBase64(args.zipBase64)\n const latest = args.latest ?? true\n const visibility = args.visibility ?? \"organization\"\n const archiveName = sanitizeArchiveFileName(args.archiveFileName, \"bundle.zip\")\n const uploaded = await uploadVersionZipBuffer({\n ctx,\n org: args.org,\n project: args.project,\n version: args.version,\n zipBuffer: buf,\n archiveFileName: archiveName,\n latest,\n })\n const updated = await patchProjectVisibility({\n ctx,\n org: args.org,\n project: args.project,\n visibility,\n })\n return jsonText({\n upload: uploaded,\n projectPatch: updated,\n versionUrl: uploaded.urls.version,\n latestUrl: uploaded.urls.latest,\n visibility,\n })\n } catch (err) {\n return toolFail(toolErrorMessage(err))\n }\n })\n )\n\n mcpServer.registerTool(\n \"delete_version\",\n {\n description:\n \"Delete one bundle version from R2/registry (not the literal latest alias slug).\",\n inputSchema: {\n org: orgSchema,\n project: projectSchema,\n version: versionSchema.describe(\n \"Concrete version slug to remove.\"\n ),\n },\n },\n async (args: { org: string; project: string; version: string }) =>\n withAuth(async (ctx) => {\n try {\n await deleteRemoteVersion({\n ctx,\n org: args.org,\n project: args.project,\n version: args.version,\n })\n return jsonText({ ok: true, deleted: args.version })\n } catch (err) {\n return toolFail(toolErrorMessage(err))\n }\n })\n )\n\n mcpServer.registerTool(\n \"get_download_url\",\n {\n description:\n \"Mint a signed same-origin bundle download URL (valid ~15 minutes).\",\n inputSchema: {\n org: orgSchema,\n project: projectSchema,\n version: versionSchema,\n },\n },\n async (args: { org: string; project: string; version: string }) =>\n withAuth(async (ctx) => {\n try {\n const meta = await mintBundleDownload({\n ctx,\n org: args.org,\n project: args.project,\n version: args.version,\n })\n return jsonText(meta)\n } catch (err) {\n return toolFail(toolErrorMessage(err))\n }\n })\n )\n\n mcpServer.registerTool(\n \"upload_starter\",\n {\n description:\n \"Upload a starter/scaffold zip for the project (any zip layout permitted).\",\n inputSchema: {\n org: orgSchema,\n project: projectSchema,\n slug: starterSlugSchema,\n zipPath: zipPathSchema,\n },\n },\n async (args: { org: string; project: string; slug: string; zipPath: string }) =>\n withAuth(async (ctx) => {\n try {\n const zipPath = await resolveVerifiedZipPath(args.zipPath)\n const payload = await uploadStarterZip({\n ctx,\n org: args.org,\n project: args.project,\n slug: args.slug,\n zipPath,\n })\n return jsonText(payload)\n } catch (err) {\n return toolFail(toolErrorMessage(err))\n }\n })\n )\n\n mcpServer.registerTool(\n \"upload_starter_base64\",\n {\n description:\n \"Same as upload_starter but ZIP is zipBase64 (for remote sandboxes).\",\n inputSchema: {\n org: orgSchema,\n project: projectSchema,\n slug: starterSlugSchema,\n zipBase64: zipBase64Schema,\n archiveFileName: archiveFileNameSchema,\n },\n },\n async (args: {\n org: string\n project: string\n slug: string\n zipBase64: string\n archiveFileName?: string\n }) =>\n withAuth(async (ctx) => {\n try {\n const buf = decodeZipBase64(args.zipBase64)\n const name = sanitizeArchiveFileName(args.archiveFileName, \"starter.zip\")\n const payload = await uploadStarterZipBuffer({\n ctx,\n org: args.org,\n project: args.project,\n slug: args.slug,\n zipBuffer: buf,\n archiveFileName: name,\n })\n return jsonText(payload)\n } catch (err) {\n return toolFail(toolErrorMessage(err))\n }\n })\n )\n\n mcpServer.registerTool(\n \"release\",\n {\n description:\n \"Upload preview bundle zip then starter zip (recommended order — matches CLI release).\",\n inputSchema: {\n org: orgSchema,\n project: projectSchema,\n version: versionSchema.describe(\n 'Preview bundle version (never the literal slug \"latest\").'\n ),\n bundleZipPath: zipPathSchema.describe(\"Path to preview/site zip.\"),\n starterZipPath: zipPathSchema.describe(\"Path to starter/template zip.\"),\n starterSlug: starterSlugSchema\n .optional()\n .describe('Starter slug; default \"source\".'),\n latest: optionalLatestSchema,\n },\n },\n async (args: {\n org: string\n project: string\n version: string\n bundleZipPath: string\n starterZipPath: string\n starterSlug?: string\n latest?: boolean\n }) =>\n withAuth(async (ctx) => {\n try {\n const bundleZipPath = await resolveVerifiedZipPath(args.bundleZipPath)\n const starterZipPath = await resolveVerifiedZipPath(args.starterZipPath)\n const latest = args.latest ?? true\n const slug = args.starterSlug ?? \"source\"\n const bundle = await uploadBundleVersion({\n ctx,\n org: args.org,\n project: args.project,\n version: args.version,\n zipPath: bundleZipPath,\n latest,\n })\n const starter = await uploadStarterZip({\n ctx,\n org: args.org,\n project: args.project,\n slug,\n zipPath: starterZipPath,\n })\n return jsonText({ bundle, starter })\n } catch (err) {\n return toolFail(toolErrorMessage(err))\n }\n })\n )\n\n mcpServer.registerTool(\n \"release_base64\",\n {\n description:\n \"Like release but both zips as base64 (bundle then starter — sandbox-safe). Same payload-size caveats as other *_base64 tools.\",\n inputSchema: {\n org: orgSchema,\n project: projectSchema,\n version: versionSchema.describe(\n 'Preview bundle version (never the literal slug \"latest\").'\n ),\n bundleZipBase64: zipBase64Schema.describe(\"Base64-encoded preview/site zip.\"),\n starterZipBase64: zipBase64Schema.describe(\n \"Base64-encoded starter zip.\"\n ),\n bundleArchiveFileName: archiveFileNameSchema.optional(),\n starterArchiveFileName: archiveFileNameSchema.optional(),\n starterSlug: starterSlugSchema.optional(),\n latest: optionalLatestSchema,\n },\n },\n async (args: {\n org: string\n project: string\n version: string\n bundleZipBase64: string\n starterZipBase64: string\n bundleArchiveFileName?: string\n starterArchiveFileName?: string\n starterSlug?: string\n latest?: boolean\n }) =>\n withAuth(async (ctx) => {\n try {\n const bundleBuf = decodeZipBase64(args.bundleZipBase64)\n const starterBuf = decodeZipBase64(args.starterZipBase64)\n const latest = args.latest ?? true\n const slug = args.starterSlug ?? \"source\"\n const bundleName = sanitizeArchiveFileName(\n args.bundleArchiveFileName,\n \"bundle.zip\"\n )\n const starterName = sanitizeArchiveFileName(\n args.starterArchiveFileName,\n \"starter.zip\"\n )\n\n const bundle = await uploadVersionZipBuffer({\n ctx,\n org: args.org,\n project: args.project,\n version: args.version,\n zipBuffer: bundleBuf,\n archiveFileName: bundleName,\n latest,\n })\n const starter = await uploadStarterZipBuffer({\n ctx,\n org: args.org,\n project: args.project,\n slug,\n zipBuffer: starterBuf,\n archiveFileName: starterName,\n })\n return jsonText({ bundle, starter })\n } catch (err) {\n return toolFail(toolErrorMessage(err))\n }\n })\n )\n\n return mcpServer\n}\n","import path from \"node:path\"\n\n/** Multipart filename only — strips directory segments and backslashes. */\nexport function sanitizeArchiveFileName(\n input: string | undefined,\n fallback: string\n): string {\n const raw = (input?.trim() || fallback).replace(/\\\\/g, \"/\")\n const base = path.basename(raw)\n if (!base || base === \".\" || base === \"..\") return fallback\n return base.length > 200 ? base.slice(0, 200) : base\n}\n","/** MCP onboarding text for clients — mirrors Quick Proto CLI constraints. */\nexport const SERVER_INSTRUCTIONS = `\nQuick Proto MCP publishes static SPA builds (React/Vite/Next static export etc.) so reviewers get HTTPS preview URLs on quick-proto.co.uk.\n\nAUTH: Requires a Better Auth API key. Use QP_API_KEY, ~/.config/quick-proto/credentials.json from \\`npx quick-proto-cli auth login\\`, or create a key in the dashboard → API keys.\n\nWORKFLOW:\n1. Call list_orgs if you do not know the org slug.\n2. Zip the built site from *inside* the output folder so index.html is at zip root:\n \\`(cd dist && zip -r ../bundle.zip .)\\`\n3a. If MCP runs on YOUR machine and the zip exists there: use publish_prototype / upload_version with zipPath (path must be under cwd, system temp, or QUICK_PROTO_UPLOAD_ROOT — only real ZIP files).\n3b. If you built the zip in a REMOTE sandbox (e.g. /home/claude/...): read file as bytes, base64-encode, call publish_prototype_base64 or upload_version_base64 — do NOT pass sandbox paths to zipPath.\n4. Version slug MUST NOT be the literal string \"latest\" — that alias is rolling only.\n5. publish_prototype / publish_prototype_base64 default visibility is organization. Pass visibility public only when you intentionally want anyone-with-link access.\n\nRULES:\n- Zip must contain index.html (root or exactly one top-level folder).\n- Max upload size matches platform limits; malformed zips → 422.\n- Base64 tools are for small/medium zips; Claude may limit tool argument size — very large builds need a local zipPath or another transfer path.\n`.trim()\n\nexport const MISSING_API_KEY_MESSAGE = `No API key configured.\n\nCreate one at https://quick-proto.co.uk (dashboard → API keys), then either:\n• Set environment variable QP_API_KEY before starting this MCP server, or\n• Run \\`printf '%s\\\\n' '{\"apiKey\":\"<key>\"}' | npx quick-proto-cli auth login --stdin\\` so credentials save to ~/.config/quick-proto/credentials.json`\n","import type { ProjectBundlePayload } from \"quick-proto-client\"\n\n/** Bundle listing without org member emails or full uploader PII — safer for LLM tool output. */\nexport type SlimProjectBundlePayload = {\n project: string\n org: string\n visibility: ProjectBundlePayload[\"visibility\"]\n latestUrl: string\n versions: Array<{\n id: string\n slug: string\n sizeBytes: number\n isLatest: boolean\n createdAt: string\n uploadedBy: { id: string; name: string }\n url: string\n }>\n starters: Array<{\n id: string\n slug: string\n sizeBytes: number\n createdAt: string\n uploadedBy: { id: string; name: string }\n }>\n}\n\nexport function slimProjectBundleForMcp(\n p: ProjectBundlePayload\n): SlimProjectBundlePayload {\n return {\n project: p.project,\n org: p.org,\n visibility: p.visibility,\n latestUrl: p.latestUrl,\n versions: p.versions.map((v) => ({\n id: v.id,\n slug: v.slug,\n sizeBytes: v.sizeBytes,\n isLatest: v.isLatest,\n createdAt: v.createdAt,\n uploadedBy: { id: v.uploadedBy.id, name: v.uploadedBy.name },\n url: v.url,\n })),\n starters: p.starters.map((s) => ({\n id: s.id,\n slug: s.slug,\n sizeBytes: s.sizeBytes,\n createdAt: s.createdAt,\n uploadedBy: { id: s.uploadedBy.id, name: s.uploadedBy.name },\n })),\n }\n}\n","import { z } from \"zod\"\n\n/** Matches apps/web preview slug rules (`PREVIEW_SLUG_SEGMENT_PATTERN`, max 40). */\nconst PREVIEW_SLUG_SEGMENT = /^[a-z0-9]+(?:-[a-z0-9]+)*$/\n\nexport const MAX_SLUG_LEN = 40\n\nconst slugMessage =\n \"Lowercase alphanumeric, hyphen-separated runs only (no leading hyphen, no `--`).\"\n\nexport const visibilitySchema = z.enum([\n \"public\",\n \"organization\",\n \"invite_only\",\n \"private\",\n])\n\nexport const orgSchema = z\n .string()\n .trim()\n .min(1)\n .max(MAX_SLUG_LEN)\n .regex(PREVIEW_SLUG_SEGMENT, slugMessage)\n .describe(\"Organization slug.\")\n\nexport const projectSchema = z\n .string()\n .trim()\n .min(1)\n .max(MAX_SLUG_LEN)\n .regex(PREVIEW_SLUG_SEGMENT, slugMessage)\n .describe(\"Project slug.\")\n\nexport const versionSchema = z\n .string()\n .trim()\n .min(1)\n .max(MAX_SLUG_LEN)\n .regex(PREVIEW_SLUG_SEGMENT, slugMessage)\n .refine((v) => v !== \"latest\", {\n message:\n 'Version slug must not be the literal \"latest\" (that slug is the rolling alias only).',\n })\n .describe(\"Bundle version slug.\")\n\nexport const starterSlugSchema = z\n .string()\n .trim()\n .min(1)\n .max(MAX_SLUG_LEN)\n .regex(PREVIEW_SLUG_SEGMENT, slugMessage)\n .describe(\"Starter template slug.\")\n\nexport const zipPathSchema = z\n .string()\n .trim()\n .min(1)\n .describe(\n \"Path to a .zip under cwd, tmpdir, or QUICK_PROTO_UPLOAD_ROOT (must be a real ZIP file).\"\n )\n\n/** Base64 of a zip archive (preferred when the agent built the zip in a remote sandbox — no shared path with MCP). May be `data:application/zip;base64,...`. */\nexport const zipBase64Schema = z.string().trim().min(1)\n\nexport const archiveFileNameSchema = z\n .string()\n .trim()\n .min(1)\n .max(200)\n .optional()\n .describe('Filename for multipart upload (default \"bundle.zip\" / \"starter.zip\").')\n\nexport const optionalLatestSchema = z\n .boolean()\n .optional()\n .describe(\n 'If true, mark this uploaded version as the rolling \"latest\" alias (defaults to true when omitted).'\n )\n","import { FetchHttpError } from \"quick-proto-client\"\n\nconst HTTP_BODY_SNIPPET_MAX = 300\n\nexport function jsonText(data: unknown): { content: [{ type: \"text\"; text: string }] } {\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(data, null, 2),\n },\n ],\n }\n}\n\nfunction truncateHttpBodySnippet(body: string): string {\n const t = body.trim()\n if (!t) return \"\"\n let snippet = t.slice(0, HTTP_BODY_SNIPPET_MAX)\n try {\n const parsed: unknown = JSON.parse(t)\n if (parsed !== null && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n const redacted = { ...(parsed as Record<string, unknown>) }\n for (const key of Object.keys(redacted)) {\n if (/stack|hint|detail|internal/i.test(key)) {\n delete redacted[key]\n }\n }\n snippet = JSON.stringify(redacted).slice(0, HTTP_BODY_SNIPPET_MAX)\n }\n } catch {\n /* keep raw slice */\n }\n return snippet.length < t.length ? `${snippet}…` : snippet\n}\n\nexport function toolErrorMessage(err: unknown): string {\n const base = err instanceof Error ? err.message : String(err)\n if (err instanceof FetchHttpError && err.body.trim() && err.body !== base) {\n const snippet = truncateHttpBodySnippet(err.body)\n return snippet ? `${base}\\nHTTP body: ${snippet}` : base\n }\n return base\n}\n\nexport function toolFail(message: string): {\n content: [{ type: \"text\"; text: string }]\n isError: boolean\n} {\n return {\n content: [{ type: \"text\", text: message }],\n isError: true,\n }\n}\n","import { constants as fsConsts } from \"node:fs\"\nimport { access, open, realpath, stat } from \"node:fs/promises\"\nimport { tmpdir } from \"node:os\"\nimport path from \"node:path\"\n\nconst ZIP_LOCAL_HEADER_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04])\n\n/** True if fileReal equals rootReal or lies under it (root is typically a directory). */\nfunction isUnderRoot(rootReal: string, fileReal: string): boolean {\n const normalizedRoot = path.normalize(rootReal)\n const normalizedFile = path.normalize(fileReal)\n if (normalizedFile === normalizedRoot) {\n return true\n }\n const prefix = normalizedRoot.endsWith(path.sep)\n ? normalizedRoot\n : normalizedRoot + path.sep\n return normalizedFile.startsWith(prefix)\n}\n\nfunction assertNotSensitiveUploadPath(canonicalPath: string): void {\n const base = path.basename(canonicalPath)\n const lowerBase = base.toLowerCase()\n if (base === \"credentials.json\") {\n throw new Error(\n \"Refusing to read credentials.json — use a build zip under your project output, not Quick Proto credentials.\"\n )\n }\n if (lowerBase === \".env\" || lowerBase.startsWith(\".env.\")) {\n throw new Error(\"Refusing to upload environment files (.env*).\")\n }\n const sep = path.sep\n const norm = canonicalPath.toLowerCase()\n const sshMarker = `${sep}.ssh${sep}`\n if (norm.includes(sshMarker) || norm.endsWith(`${sep}.ssh`)) {\n throw new Error(\"Refusing to read paths under .ssh.\")\n }\n}\n\nasync function readZipMagicOk(filePath: string): Promise<void> {\n const fh = await open(filePath, \"r\")\n try {\n const buf = Buffer.alloc(4)\n const { bytesRead } = await fh.read(buf, 0, 4, 0)\n if (\n bytesRead < 4 ||\n buf[0] !== ZIP_LOCAL_HEADER_MAGIC[0] ||\n buf[1] !== ZIP_LOCAL_HEADER_MAGIC[1] ||\n buf[2] !== ZIP_LOCAL_HEADER_MAGIC[2] ||\n buf[3] !== ZIP_LOCAL_HEADER_MAGIC[3]\n ) {\n throw new Error(\n \"File does not look like a ZIP archive (missing PK\\\\x03\\\\x04 local header).\"\n )\n }\n } finally {\n await fh.close().catch(() => {})\n }\n}\n\nasync function resolveAllowedRoots(): Promise<string[]> {\n const raw = process.env.QUICK_PROTO_UPLOAD_ROOT?.trim()\n if (raw) {\n const parts = raw\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean)\n const roots: string[] = []\n for (const p of parts) {\n if (!path.isAbsolute(p)) {\n throw new Error(\n `QUICK_PROTO_UPLOAD_ROOT entries must be absolute paths; got \"${p}\".`\n )\n }\n let rp: string\n try {\n rp = await realpath(p)\n } catch {\n throw new Error(\n `QUICK_PROTO_UPLOAD_ROOT path not found or inaccessible: \"${p}\".`\n )\n }\n const st = await stat(rp)\n if (!st.isDirectory()) {\n throw new Error(`QUICK_PROTO_UPLOAD_ROOT must be a directory: \"${p}\".`)\n }\n roots.push(rp)\n }\n return [...new Set(roots)]\n }\n\n const cwd = await realpath(process.cwd())\n let tmp: string\n try {\n tmp = await realpath(tmpdir())\n } catch {\n tmp = path.normalize(tmpdir())\n }\n return cwd === tmp ? [cwd] : [cwd, tmp]\n}\n\n/**\n * Resolve user-supplied zip path: realpath, regular file, allowed roots, deny sensitive paths, ZIP magic.\n * Returns canonical filesystem path safe to pass to {@link readFile}.\n */\nexport async function resolveVerifiedZipPath(userPath: string): Promise<string> {\n const trimmed = userPath.trim()\n if (!trimmed) {\n throw new Error(\"ZIP path is empty.\")\n }\n\n const resolved = path.resolve(process.cwd(), trimmed)\n\n let canonical: string\n try {\n canonical = await realpath(resolved)\n } catch {\n throw new Error(`ZIP path not found or inaccessible: ${trimmed}`)\n }\n\n const st = await stat(canonical)\n if (!st.isFile()) {\n throw new Error(`ZIP path must be a regular file: ${canonical}`)\n }\n\n await access(canonical, fsConsts.R_OK)\n\n assertNotSensitiveUploadPath(canonical)\n\n const roots = await resolveAllowedRoots()\n const allowed = roots.some((root) => isUnderRoot(root, canonical))\n if (!allowed) {\n throw new Error(\n `ZIP path is outside allowed directories (${roots.join(\", \")}). Set QUICK_PROTO_UPLOAD_ROOT to extend. Resolved: ${canonical}`\n )\n }\n\n await readZipMagicOk(canonical)\n\n return canonical\n}\n","import { Buffer } from \"node:buffer\"\n\nimport { MAX_ZIP_UPLOAD_BYTES } from \"quick-proto-client\"\n\n/**\n * Max base64 payload characters (after stripping `data:…;base64,`) before `Buffer.from`.\n * Slightly above 4/3 × zip limit so legitimate uploads succeed; blocks hostile megapixel strings.\n */\nexport const MAX_ZIP_BASE64_PAYLOAD_CHARS =\n Math.ceil((MAX_ZIP_UPLOAD_BYTES * 4) / 3) + 128\n\n/**\n * Decode ZIP file contents passed through MCP JSON (sandbox → local server has no shared disk).\n * Supports raw base64 or `data:application/zip;base64,...` payloads.\n */\nexport function decodeZipBase64(input: string): Buffer {\n const trimmed = input.trim()\n if (!trimmed) {\n throw new Error(\"zipBase64 is empty.\")\n }\n const payload =\n /^data:[^;]+;base64,/i.test(trimmed)\n ? trimmed.replace(/^[^,]+,/, \"\")\n : trimmed.replace(/\\s+/g, \"\")\n\n if (payload.length > MAX_ZIP_BASE64_PAYLOAD_CHARS) {\n throw new Error(\n `zipBase64 exceeds maximum encoded length (${MAX_ZIP_BASE64_PAYLOAD_CHARS} characters).`\n )\n }\n\n const buf = Buffer.from(payload, \"base64\")\n if (buf.byteLength === 0) {\n throw new Error(\"Decoded base64 is empty — check zipBase64 is valid ZIP base64.\")\n }\n // PK\\x03\\x04 — local file header magic (normal zip files)\n if (!(buf[0] === 0x50 && buf[1] === 0x4b)) {\n throw new Error(\n \"Decoded bytes do not look like a zip file (missing PK header). Wrong encoding or truncation.\"\n )\n }\n return buf\n}\n"],"mappings":"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","names":["path","Buffer"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "quick-proto-mcp",
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"description": "MCP server (stdio) for Quick Proto — upload preview bundles and get shareable URLs from Claude Desktop and other MCP clients",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Quick Proto contributors",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/williammcdowell/quick-proto-v2.git",
|
|
11
|
+
"directory": "packages/mcp"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/williammcdowell/quick-proto-v2/tree/main/packages/mcp#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/williammcdowell/quick-proto-v2/issues"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
},
|
|
20
|
+
"files": ["dist", "LICENSE", "README.md"],
|
|
21
|
+
"bin": {
|
|
22
|
+
"quick-proto-mcp": "dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"prepack": "npm run build",
|
|
27
|
+
"prepublishOnly": "npm run build",
|
|
28
|
+
"lint": "eslint",
|
|
29
|
+
"format": "prettier --write \"**/*.{ts,mjs}\"",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "vitest run"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
35
|
+
"quick-proto-client": "^0.1.0",
|
|
36
|
+
"zod": "^3.25.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^25.1.0",
|
|
40
|
+
"@workspace/eslint-config": "*",
|
|
41
|
+
"@workspace/typescript-config": "*",
|
|
42
|
+
"eslint": "^9.39.2",
|
|
43
|
+
"prettier": "^3.8.1",
|
|
44
|
+
"tsup": "^8.5.1",
|
|
45
|
+
"typescript": "^5.9.3",
|
|
46
|
+
"vitest": "^4.1.6"
|
|
47
|
+
}
|
|
48
|
+
}
|