@dypai-ai/mcp 1.0.6 → 1.0.7
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/package.json +1 -1
- package/src/index.js +7 -4
- package/src/tools/deploy.js +158 -21
- package/src/tools/github.js +57 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -138,10 +138,13 @@ const SERVER_INSTRUCTIONS = `You are building full-stack applications on the DYP
|
|
|
138
138
|
|
|
139
139
|
## Deploy Frontend
|
|
140
140
|
- Use deploy_frontend tool with the project's source directory
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
-
|
|
141
|
+
- The tool pushes source, then waits for the build to finish (~1-2 min)
|
|
142
|
+
- If the build succeeds, the response includes the live URL
|
|
143
|
+
- If the build FAILS, the response includes the error logs — read them carefully, fix the code, and redeploy
|
|
144
|
+
- Common build failures: missing dependencies (add to package.json), TypeScript errors, import path issues
|
|
145
|
+
- Supports: Vite, React, Next.js, Astro, SvelteKit, Nuxt, Remix, Angular, Vinext, CRA, and more
|
|
146
|
+
- Framework is auto-detected from package.json — correct build config is set automatically
|
|
147
|
+
- If build times out (>2 min), use get_build_status / get_deployment_logs to check manually
|
|
145
148
|
|
|
146
149
|
## Import Data
|
|
147
150
|
- Use bulk_upsert to import CSV or JSON files into database tables
|
package/src/tools/deploy.js
CHANGED
|
@@ -11,14 +11,51 @@ import { join, relative } from "path"
|
|
|
11
11
|
import { api } from "../api.js"
|
|
12
12
|
|
|
13
13
|
const MAX_SOURCE_SIZE = 50 * 1024 * 1024
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
|
|
15
|
+
// Directories to skip — build outputs, caches, package managers
|
|
16
|
+
const IGNORE_DIRS = new Set([
|
|
17
|
+
"node_modules", ".git",
|
|
18
|
+
// Build outputs
|
|
19
|
+
"dist", "build", "out", ".output", ".vercel", ".netlify",
|
|
20
|
+
// Framework caches
|
|
21
|
+
".next", ".nuxt", ".svelte-kit", ".astro", ".angular", ".docusaurus",
|
|
22
|
+
".cache", ".turbo", ".vite", ".parcel-cache", ".wrangler",
|
|
23
|
+
// Test / misc
|
|
24
|
+
"coverage", "storybook-static", "__pycache__", ".idea", ".vscode",
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
// Source file extensions to include
|
|
28
|
+
const SOURCE_EXTS = new Set([
|
|
29
|
+
// JavaScript / TypeScript
|
|
30
|
+
".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts", ".cjs", ".cts",
|
|
31
|
+
// Styles
|
|
32
|
+
".css", ".scss", ".less", ".sass", ".styl",
|
|
33
|
+
// Markup / templates
|
|
34
|
+
".html", ".htm",
|
|
35
|
+
// Framework-specific SFCs
|
|
36
|
+
".vue", ".svelte", ".astro",
|
|
37
|
+
// Data / config
|
|
38
|
+
".json", ".toml", ".yaml", ".yml",
|
|
39
|
+
// Content
|
|
40
|
+
".md", ".mdx", ".txt",
|
|
41
|
+
// Assets metadata
|
|
42
|
+
".svg", ".ico", ".webmanifest",
|
|
43
|
+
// Schema / query
|
|
44
|
+
".graphql", ".gql", ".prisma",
|
|
45
|
+
// Misc
|
|
46
|
+
".xml", ".csv",
|
|
47
|
+
])
|
|
48
|
+
|
|
16
49
|
const IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".ico", ".svg"])
|
|
17
50
|
const BLOCKED = new Set([".env", ".env.local", ".env.production", ".env.development", ".DS_Store", "Thumbs.db"])
|
|
18
51
|
const MAX_IMAGE = 2 * 1024 * 1024
|
|
19
52
|
|
|
53
|
+
// Root-level config files to always include (regex)
|
|
54
|
+
const ROOT_CONFIG_RE = /^(package\.json|package-lock\.json|pnpm-lock\.yaml|bun\.lockb|yarn\.lock|vite\.config|vitest\.config|tsconfig|tailwind\.config|postcss\.config|next\.config|vinext\.config|astro\.config|nuxt\.config|svelte\.config|remix\.config|gatsby-config|angular\.json|docusaurus\.config|wrangler\.toml|wrangler\.json|vercel\.json|netlify\.toml|turbo\.json|components\.json|uno\.config|eslint\.config|prettier\.config|jest\.config|playwright\.config|\.prettierrc|\.eslintrc|\.browserslistrc|\.nvmrc|\.node-version|index\.html)/
|
|
55
|
+
|
|
20
56
|
function collectSource(dir) {
|
|
21
57
|
const files = []
|
|
58
|
+
const skipped = []
|
|
22
59
|
let total = 0
|
|
23
60
|
|
|
24
61
|
function walk(d, rel) {
|
|
@@ -35,36 +72,60 @@ function collectSource(dir) {
|
|
|
35
72
|
if (!stat.isFile()) continue
|
|
36
73
|
const ext = entry.includes(".") ? `.${entry.split(".").pop().toLowerCase()}` : ""
|
|
37
74
|
let ok = SOURCE_EXTS.has(ext)
|
|
38
|
-
if (!rel &&
|
|
75
|
+
if (!rel && ROOT_CONFIG_RE.test(entry)) ok = true
|
|
39
76
|
if (IMAGE_EXTS.has(ext) && stat.size <= MAX_IMAGE) ok = true
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
77
|
+
|
|
78
|
+
// Track skipped files so the user knows what was excluded
|
|
79
|
+
if (!ok) {
|
|
80
|
+
if (IMAGE_EXTS.has(ext) && stat.size > MAX_IMAGE) {
|
|
81
|
+
skipped.push({ path, reason: `Image too large (${(stat.size / (1024 * 1024)).toFixed(1)}MB, max ${MAX_IMAGE / (1024 * 1024)}MB)` })
|
|
82
|
+
} else if (ext && ![".lock", ".log"].includes(ext)) {
|
|
83
|
+
skipped.push({ path, reason: `Unsupported file type (${ext})` })
|
|
84
|
+
}
|
|
85
|
+
continue
|
|
45
86
|
}
|
|
87
|
+
|
|
88
|
+
const content = readFileSync(full)
|
|
89
|
+
if (total + content.length > MAX_SOURCE_SIZE) {
|
|
90
|
+
skipped.push({ path, reason: "Total upload size limit reached" })
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
total += content.length
|
|
94
|
+
files.push({ path, content: content.toString("base64") })
|
|
46
95
|
} catch {}
|
|
47
96
|
}
|
|
48
97
|
}
|
|
49
98
|
|
|
50
99
|
walk(dir, "")
|
|
51
|
-
return { files, total }
|
|
100
|
+
return { files, total, skipped }
|
|
52
101
|
}
|
|
53
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Detect framework for display AND for API build config.
|
|
105
|
+
* Returns { label, id } where id matches pages_service.py FRAMEWORK_PRESETS.
|
|
106
|
+
*/
|
|
54
107
|
function detectFramework(dir) {
|
|
55
108
|
const pkgPath = join(dir, "package.json")
|
|
56
109
|
if (!existsSync(pkgPath)) return null
|
|
57
110
|
try {
|
|
58
111
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"))
|
|
59
112
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
60
|
-
if (deps["
|
|
61
|
-
if (deps["
|
|
62
|
-
if (deps["
|
|
63
|
-
if (deps["
|
|
64
|
-
if (deps["
|
|
65
|
-
if (deps["
|
|
66
|
-
if (deps["
|
|
67
|
-
return "
|
|
113
|
+
if (deps["vinext"]) return { label: "Vinext", id: "vinext" }
|
|
114
|
+
if (deps["next"]) return { label: "Next.js", id: "next" }
|
|
115
|
+
if (deps["@sveltejs/kit"]) return { label: "SvelteKit", id: "sveltekit" }
|
|
116
|
+
if (deps["nuxt"]) return { label: "Nuxt", id: "nuxt" }
|
|
117
|
+
if (deps["@remix-run/react"])return { label: "Remix", id: "remix" }
|
|
118
|
+
if (deps["astro"]) return { label: "Astro", id: "astro" }
|
|
119
|
+
if (deps["gatsby"]) return { label: "Gatsby", id: "gatsby" }
|
|
120
|
+
if (deps["@angular/core"]) return { label: "Angular", id: "angular" }
|
|
121
|
+
if (deps["@docusaurus/core"])return { label: "Docusaurus", id: "docusaurus" }
|
|
122
|
+
if (deps["preact"]) return { label: "Preact", id: "preact" }
|
|
123
|
+
if (deps["solid-js"]) return { label: "Solid", id: "solid" }
|
|
124
|
+
if (deps["react-scripts"]) return { label: "Create React App",id: "cra" }
|
|
125
|
+
if (deps["vite"]) return { label: "Vite", id: "vite" }
|
|
126
|
+
if (deps["vue"]) return { label: "Vue", id: "vue" }
|
|
127
|
+
if (deps["react"]) return { label: "React", id: "react" }
|
|
128
|
+
return { label: "Node.js", id: "vite" }
|
|
68
129
|
} catch { return null }
|
|
69
130
|
}
|
|
70
131
|
|
|
@@ -105,7 +166,7 @@ After deploying, the site is live at https://{slug}.dypai.app within ~1 minute.`
|
|
|
105
166
|
}
|
|
106
167
|
|
|
107
168
|
const framework = detectFramework(sourceDirectory)
|
|
108
|
-
const { files, total } = collectSource(sourceDirectory)
|
|
169
|
+
const { files, total, skipped } = collectSource(sourceDirectory)
|
|
109
170
|
|
|
110
171
|
if (!files.length) {
|
|
111
172
|
return { error: "No source files found." }
|
|
@@ -114,17 +175,93 @@ After deploying, the site is live at https://{slug}.dypai.app within ~1 minute.`
|
|
|
114
175
|
try {
|
|
115
176
|
const result = await api.post(
|
|
116
177
|
`/api/engine/${project_id}/frontend/deploy/source`,
|
|
117
|
-
{ files }
|
|
178
|
+
{ files, framework: framework?.id ?? null }
|
|
118
179
|
)
|
|
119
180
|
|
|
181
|
+
const label = framework?.label ?? "Project"
|
|
182
|
+
|
|
183
|
+
// ── Wait for CF Pages build (poll up to ~2 min) ──────────────────────
|
|
184
|
+
let buildResult = null
|
|
185
|
+
const maxAttempts = 12 // 12 × 10s = 2 min max
|
|
186
|
+
const pollInterval = 10_000 // 10 seconds
|
|
187
|
+
|
|
188
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
189
|
+
await new Promise(r => setTimeout(r, pollInterval))
|
|
190
|
+
try {
|
|
191
|
+
const status = await api.get(`/api/engine/${project_id}/frontend/build-status`)
|
|
192
|
+
const s = status?.status
|
|
193
|
+
if (s === "success") {
|
|
194
|
+
buildResult = { status: "success", url: result.url, duration: status.duration_seconds }
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
if (s === "failure") {
|
|
198
|
+
// Fetch build logs for error context
|
|
199
|
+
let logs = status.build_error || null
|
|
200
|
+
if (!logs) {
|
|
201
|
+
try {
|
|
202
|
+
const deployments = await api.get(`/api/engine/${project_id}/frontend/deployments?limit=1`)
|
|
203
|
+
const depId = deployments?.deployments?.[0]?.id
|
|
204
|
+
if (depId) {
|
|
205
|
+
const logRes = await api.get(`/api/engine/${project_id}/frontend/deployments/${depId}/logs`)
|
|
206
|
+
if (logRes?.logs) {
|
|
207
|
+
const lines = logRes.logs.split("\n")
|
|
208
|
+
logs = lines.slice(-30).join("\n")
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} catch {}
|
|
212
|
+
}
|
|
213
|
+
buildResult = { status: "failure", error: logs || "Build failed — check logs with get_deployment_logs" }
|
|
214
|
+
break
|
|
215
|
+
}
|
|
216
|
+
// still queued/building — keep polling
|
|
217
|
+
} catch {
|
|
218
|
+
// build-status not available yet, keep polling
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!buildResult) {
|
|
223
|
+
// Timed out — build still in progress
|
|
224
|
+
return {
|
|
225
|
+
success: true,
|
|
226
|
+
url: result.url,
|
|
227
|
+
files_pushed: files.length,
|
|
228
|
+
size_bytes: total,
|
|
229
|
+
framework: label,
|
|
230
|
+
build: "cloudflare_pages",
|
|
231
|
+
build_status: "building",
|
|
232
|
+
message: `Deployed ${files.length} files (${Math.round(total / 1024)} KB). ${label} build still in progress at ${result.url} — use get_build_status to check progress.`,
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (buildResult.status === "failure") {
|
|
237
|
+
return {
|
|
238
|
+
success: false,
|
|
239
|
+
url: result.url,
|
|
240
|
+
files_pushed: files.length,
|
|
241
|
+
framework: label,
|
|
242
|
+
build: "cloudflare_pages",
|
|
243
|
+
build_status: "failure",
|
|
244
|
+
build_error: buildResult.error,
|
|
245
|
+
message: `Deploy pushed ${files.length} files but the ${label} build FAILED. Build error:\n${buildResult.error}`,
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const skippedMsg = skipped.length > 0
|
|
250
|
+
? `\n\nSkipped ${skipped.length} file(s):\n${skipped.map(s => ` - ${s.path}: ${s.reason}`).join("\n")}`
|
|
251
|
+
: ""
|
|
252
|
+
|
|
120
253
|
return {
|
|
121
254
|
success: true,
|
|
122
255
|
url: result.url,
|
|
123
256
|
files_pushed: files.length,
|
|
257
|
+
files_skipped: skipped.length,
|
|
258
|
+
skipped_files: skipped.length > 0 ? skipped : undefined,
|
|
124
259
|
size_bytes: total,
|
|
125
|
-
framework:
|
|
260
|
+
framework: label,
|
|
126
261
|
build: "cloudflare_pages",
|
|
127
|
-
|
|
262
|
+
build_status: "success",
|
|
263
|
+
build_duration: buildResult.duration,
|
|
264
|
+
message: `Deployed ${files.length} files (${Math.round(total / 1024)} KB). ${label} build succeeded${buildResult.duration ? ` in ${buildResult.duration}s` : ""}. Live at ${result.url}${skippedMsg}`,
|
|
128
265
|
}
|
|
129
266
|
} catch (e) {
|
|
130
267
|
return { error: `Deploy failed: ${e.message}` }
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub repo link/unlink tools.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { api } from "../api.js"
|
|
6
|
+
|
|
7
|
+
export const linkRepoTool = {
|
|
8
|
+
name: "link_repo",
|
|
9
|
+
description: `Link an existing GitHub repo to the project.
|
|
10
|
+
|
|
11
|
+
After linking, pushes to that repo auto-deploy to Cloudflare Pages.
|
|
12
|
+
The MCP deploy tool will also push to this repo.
|
|
13
|
+
Requires the user to have GitHub connected (DYPAI App installed on their account).`,
|
|
14
|
+
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
project_id: { type: "string", description: "Project UUID." },
|
|
19
|
+
repo_url: { type: "string", description: "GitHub repo URL (e.g. https://github.com/owner/repo)" },
|
|
20
|
+
branch: { type: "string", description: "Branch to deploy from (default: main)" },
|
|
21
|
+
},
|
|
22
|
+
required: ["project_id", "repo_url"],
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
async execute({ project_id, repo_url, branch }) {
|
|
26
|
+
try {
|
|
27
|
+
return await api.post(`/api/engine/${project_id}/github/link`, {
|
|
28
|
+
repo_url,
|
|
29
|
+
branch: branch || "main",
|
|
30
|
+
})
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return { error: e.message }
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const unlinkRepoTool = {
|
|
38
|
+
name: "unlink_repo",
|
|
39
|
+
description: `Unlink the user's GitHub repo and revert to a DYPAI-managed repo.
|
|
40
|
+
The user's repo is NOT deleted — only the connection is removed.`,
|
|
41
|
+
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
project_id: { type: "string", description: "Project UUID." },
|
|
46
|
+
},
|
|
47
|
+
required: ["project_id"],
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
async execute({ project_id }) {
|
|
51
|
+
try {
|
|
52
|
+
return await api.post(`/api/engine/${project_id}/github/unlink`, {})
|
|
53
|
+
} catch (e) {
|
|
54
|
+
return { error: e.message }
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
}
|