@dypai-ai/mcp 1.0.10 → 1.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.
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Codegen — generates TypeScript types + a typed api wrapper from the
3
+ * local dypai/ folder. Output lives inside src/ (by default src/dypai/)
4
+ * so it plays nicely with the user's tsconfig / bundler / `@/` alias.
5
+ *
6
+ * Reads:
7
+ * dypai/schema.sql → src/dypai/database.ts
8
+ * dypai/endpoints/**.yaml → src/dypai/api.ts
9
+ *
10
+ * Re-run is idempotent and cheap (<1s for typical projects). Auto-fired by
11
+ * pull / push / DDL so frontend types stay in sync without manual steps.
12
+ */
13
+
14
+ import { readFile, readdir, mkdir, writeFile, access } from "fs/promises"
15
+ import { join, resolve as resolvePath, dirname } from "path"
16
+ import YAML from "yaml"
17
+ import { readLocalConfig } from "./sync/planner.js"
18
+
19
+ // ─── Naming helpers ─────────────────────────────────────────────────────────
20
+
21
+ export function toCamel(name) {
22
+ return String(name).replace(/[-_](\w)/g, (_, c) => c.toUpperCase())
23
+ .replace(/^\w/, c => c.toLowerCase())
24
+ }
25
+
26
+ export function toPascal(name) {
27
+ const c = toCamel(name)
28
+ return c.charAt(0).toUpperCase() + c.slice(1)
29
+ }
30
+
31
+ // ─── SQL → TS type mapping ──────────────────────────────────────────────────
32
+
33
+ function sqlTypeToTs(sqlType) {
34
+ const t = String(sqlType).toLowerCase().trim()
35
+ if (/^(uuid|text|varchar|char|character|citext|name)/.test(t)) return "string"
36
+ if (/^(timestamp|date|time|interval)/.test(t)) return "string"
37
+ if (/^(int|smallint|bigint|integer|numeric|decimal|real|double|serial|bigserial|float|money)/.test(t)) return "number"
38
+ if (/^(boolean|bool)/.test(t)) return "boolean"
39
+ if (/^(json|jsonb)/.test(t)) return "unknown"
40
+ if (/^bytea/.test(t)) return "string"
41
+ if (/\[\]$/.test(t)) {
42
+ const inner = sqlTypeToTs(t.replace(/\[\]$/, ""))
43
+ return `${inner}[]`
44
+ }
45
+ // Enums and user-defined types → string at MVP (could be refined later)
46
+ return "string"
47
+ }
48
+
49
+ // ─── Generate database.ts from schema.sql ───────────────────────────────────
50
+
51
+ function parseCreateTable(ddl) {
52
+ // Extracts { table, columns: [{name, type, nullable, hasDefault}] }
53
+ const m = ddl.match(/CREATE\s+TABLE\s+public\.(\w+)\s*\(([\s\S]*?)\);/i)
54
+ if (!m) return null
55
+ const table = m[1]
56
+ const body = m[2]
57
+ const lines = body.split(/,\s*\n/).map(l => l.trim()).filter(Boolean)
58
+ const columns = []
59
+ for (const line of lines) {
60
+ // Skip constraint lines (PRIMARY KEY (...), FOREIGN KEY...)
61
+ if (/^(CONSTRAINT|PRIMARY KEY|UNIQUE|FOREIGN KEY|CHECK)\b/i.test(line)) continue
62
+ const colMatch = line.match(/^(\w+)\s+([^,]+?)(?:\s+(NOT NULL|NULL))?(?:\s+(PRIMARY KEY))?(?:\s+(DEFAULT\s+.+))?$/i)
63
+ if (!colMatch) {
64
+ const simple = line.match(/^(\w+)\s+(\w+(?:\s*\(\d+(?:\s*,\s*\d+)?\))?(?:\[\])?)\b/)
65
+ if (simple) {
66
+ columns.push({
67
+ name: simple[1],
68
+ type: simple[2],
69
+ nullable: !/NOT NULL/i.test(line),
70
+ hasDefault: /DEFAULT/i.test(line),
71
+ })
72
+ }
73
+ continue
74
+ }
75
+ columns.push({
76
+ name: colMatch[1],
77
+ type: colMatch[2].trim(),
78
+ nullable: !/NOT NULL/i.test(line),
79
+ hasDefault: /DEFAULT/i.test(line),
80
+ })
81
+ }
82
+ return { table, columns }
83
+ }
84
+
85
+ export function generateDatabaseTypes(schemaSql) {
86
+ // Split on the CREATE TABLE boundaries (keeping the "CREATE" prefix)
87
+ const tables = []
88
+ const re = /CREATE\s+TABLE\s+public\.\w+\s*\([\s\S]*?\);/gi
89
+ let m
90
+ while ((m = re.exec(schemaSql)) !== null) {
91
+ const parsed = parseCreateTable(m[0])
92
+ if (parsed) tables.push(parsed)
93
+ }
94
+
95
+ const lines = [
96
+ "// Auto-generated by dypai. Do NOT edit by hand — regenerated on every pull / push / DDL.",
97
+ "// Derived from dypai/schema.sql.",
98
+ "",
99
+ "export interface Database {",
100
+ " public: {",
101
+ " Tables: {",
102
+ ]
103
+
104
+ for (const t of tables) {
105
+ lines.push(` ${t.table}: {`)
106
+ // Row = exactly what comes out of SELECT
107
+ lines.push(` Row: {`)
108
+ for (const c of t.columns) {
109
+ const ts = sqlTypeToTs(c.type)
110
+ const opt = c.nullable ? " | null" : ""
111
+ lines.push(` ${c.name}: ${ts}${opt}`)
112
+ }
113
+ lines.push(` }`)
114
+ // Insert = what you can pass to INSERT (optional for nullable or with default)
115
+ lines.push(` Insert: {`)
116
+ for (const c of t.columns) {
117
+ const ts = sqlTypeToTs(c.type)
118
+ const optional = c.nullable || c.hasDefault ? "?" : ""
119
+ const or = c.nullable ? " | null" : ""
120
+ lines.push(` ${c.name}${optional}: ${ts}${or}`)
121
+ }
122
+ lines.push(` }`)
123
+ // Update = partial of Insert
124
+ lines.push(` Update: Partial<Database["public"]["Tables"]["${t.table}"]["Insert"]>`)
125
+ lines.push(` }`)
126
+ }
127
+
128
+ lines.push(" }")
129
+ lines.push(" }")
130
+ lines.push("}")
131
+ lines.push("")
132
+ return lines.join("\n")
133
+ }
134
+
135
+ // ─── JSON Schema → TS inline ────────────────────────────────────────────────
136
+
137
+ function jsonSchemaToTs(schema, indent = "") {
138
+ if (!schema || typeof schema !== "object") return "unknown"
139
+ if (Array.isArray(schema.enum) && schema.enum.length) {
140
+ return schema.enum.map(v => typeof v === "string" ? JSON.stringify(v) : String(v)).join(" | ")
141
+ }
142
+ const type = schema.type
143
+ if (type === "string") return "string"
144
+ if (type === "number" || type === "integer") return "number"
145
+ if (type === "boolean") return "boolean"
146
+ if (type === "null") return "null"
147
+ if (type === "array") {
148
+ const inner = jsonSchemaToTs(schema.items, indent)
149
+ return `Array<${inner}>`
150
+ }
151
+ if (type === "object" || schema.properties) {
152
+ const props = schema.properties || {}
153
+ const required = new Set(schema.required || [])
154
+ const keys = Object.keys(props)
155
+ if (!keys.length) return "Record<string, unknown>"
156
+ const next = indent + " "
157
+ const body = keys.map(k => {
158
+ const opt = required.has(k) ? "" : "?"
159
+ return `${next}${JSON.stringify(k)}${opt}: ${jsonSchemaToTs(props[k], next)}`
160
+ }).join("\n")
161
+ return `{\n${body}\n${indent}}`
162
+ }
163
+ // Union of types (e.g. ["object","string"])
164
+ if (Array.isArray(type)) {
165
+ return type.map(t => jsonSchemaToTs({ ...schema, type: t }, indent)).join(" | ")
166
+ }
167
+ return "unknown"
168
+ }
169
+
170
+ // ─── Generate api.ts from endpoint YAMLs ────────────────────────────────────
171
+
172
+ async function collectEndpoints(rootDir) {
173
+ const endpointsDir = join(rootDir, "endpoints")
174
+ const results = []
175
+ async function walk(dir) {
176
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => [])
177
+ for (const e of entries) {
178
+ const full = join(dir, e.name)
179
+ if (e.isDirectory()) await walk(full)
180
+ else if (e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))) {
181
+ try {
182
+ const raw = await readFile(full, "utf8")
183
+ const doc = YAML.parse(raw) || {}
184
+ if (doc.name) results.push(doc)
185
+ } catch { /* skip malformed */ }
186
+ }
187
+ }
188
+ }
189
+ await walk(endpointsDir)
190
+ return results
191
+ }
192
+
193
+ function isBodyMethod(m) {
194
+ return ["POST", "PUT", "PATCH"].includes(String(m || "GET").toUpperCase())
195
+ }
196
+
197
+ export function generateApiTypes(endpoints, sdkImport = "../lib/dypai") {
198
+ const methods = []
199
+ const usedNames = new Set()
200
+
201
+ for (const ep of endpoints) {
202
+ let methodName = toCamel(ep.name)
203
+ // Ensure JS-safe identifier
204
+ if (!/^[a-z][A-Za-z0-9]*$/.test(methodName)) {
205
+ methodName = methodName.replace(/[^A-Za-z0-9]/g, "_")
206
+ }
207
+ let unique = methodName
208
+ let i = 2
209
+ while (usedNames.has(unique)) {
210
+ unique = `${methodName}${i++}`
211
+ }
212
+ usedNames.add(unique)
213
+
214
+ const http = String(ep.method || "GET").toUpperCase()
215
+ const hasBody = isBodyMethod(http)
216
+ const inputTs = ep.input ? jsonSchemaToTs(ep.input, " ") : "void"
217
+ const outputTs = ep.output ? jsonSchemaToTs(ep.output, " ") : "unknown"
218
+
219
+ const inputParam = hasBody
220
+ ? (inputTs === "void" ? "body?: Record<string, unknown>" : `body: ${inputTs}`)
221
+ : (inputTs === "void" ? "params?: Record<string, unknown>" : `params?: ${inputTs}`)
222
+
223
+ const callArg = hasBody ? "body" : "params"
224
+ const httpLower = http.toLowerCase()
225
+
226
+ // Short JSDoc header
227
+ const desc = (ep.description || "").replace(/\*\//g, "* /")
228
+
229
+ methods.push([
230
+ ` /**`,
231
+ ` * ${http} /${ep.name}`,
232
+ desc ? ` * ${desc}` : null,
233
+ ` */`,
234
+ ` ${unique}: (${inputParam}) =>`,
235
+ ` dypai.api.${httpLower}<${outputTs}>(${JSON.stringify(ep.name)}, ${callArg}),`,
236
+ ``,
237
+ ].filter(Boolean).join("\n"))
238
+ }
239
+
240
+ return [
241
+ "// Auto-generated by dypai. Do NOT edit by hand — regenerated on every pull / push.",
242
+ "// Derived from dypai/endpoints/*.yaml.",
243
+ "",
244
+ `import { dypai } from ${JSON.stringify(sdkImport)}`,
245
+ "",
246
+ "export const api = {",
247
+ ...methods,
248
+ "} as const",
249
+ "",
250
+ ].join("\n")
251
+ }
252
+
253
+ // ─── Orchestrator ───────────────────────────────────────────────────────────
254
+
255
+ async function ensureDir(p) {
256
+ await mkdir(p, { recursive: true })
257
+ }
258
+
259
+ async function fileExists(p) {
260
+ try { await access(p); return true } catch { return false }
261
+ }
262
+
263
+ /**
264
+ * Main entry: regenerate types from the dypai/ folder into the configured
265
+ * output directory (defaults to src/dypai/ inside the same workspace).
266
+ *
267
+ * Non-destructive when the workspace structure can't be resolved (e.g. no
268
+ * src/ folder) — skips silently so the action that triggered it doesn't fail.
269
+ */
270
+ export async function regenerateTypes(rootDir, opts = {}) {
271
+ const config = await readLocalConfig(rootDir)
272
+ const codegenCfg = config?.codegen || {}
273
+ // Resolution: output_dir is relative to the WORKSPACE (parent of dypai/),
274
+ // not to dypai/ itself.
275
+ const workspace = dirname(rootDir)
276
+ const outputDir = resolvePath(workspace, codegenCfg.output_dir || "src/dypai")
277
+ const sdkImport = codegenCfg.sdk_import || "../lib/dypai"
278
+
279
+ const files_written = []
280
+ const skipped = []
281
+
282
+ // Ensure src/ exists — if not, skip with a note (probably not a typical web project)
283
+ const srcDir = resolvePath(workspace, "src")
284
+ if (!(await fileExists(srcDir)) && !codegenCfg.output_dir) {
285
+ return {
286
+ skipped: true,
287
+ reason: "no src/ folder found — skipping codegen. Set codegen.output_dir in dypai.config.yaml to override.",
288
+ }
289
+ }
290
+
291
+ await ensureDir(outputDir)
292
+
293
+ // database.ts (only if schema.sql exists)
294
+ const schemaPath = join(rootDir, "schema.sql")
295
+ if (await fileExists(schemaPath)) {
296
+ const schema = await readFile(schemaPath, "utf8")
297
+ const dbTs = generateDatabaseTypes(schema)
298
+ const dbPath = join(outputDir, "database.ts")
299
+ await writeFile(dbPath, dbTs, "utf8")
300
+ files_written.push(dbPath)
301
+ } else {
302
+ skipped.push("database.ts (no schema.sql)")
303
+ }
304
+
305
+ // api.ts (only if endpoints/ has at least one yaml)
306
+ const endpoints = await collectEndpoints(rootDir)
307
+ if (endpoints.length > 0) {
308
+ const apiTs = generateApiTypes(endpoints, sdkImport)
309
+ const apiPath = join(outputDir, "api.ts")
310
+ await writeFile(apiPath, apiTs, "utf8")
311
+ files_written.push(apiPath)
312
+ } else {
313
+ skipped.push("api.ts (no endpoint YAMLs)")
314
+ }
315
+
316
+ // index.ts
317
+ if (files_written.length > 0) {
318
+ const exports = []
319
+ if (files_written.some(f => f.endsWith("database.ts"))) exports.push('export type { Database } from "./database"')
320
+ if (files_written.some(f => f.endsWith("api.ts"))) exports.push('export { api } from "./api"')
321
+ const indexPath = join(outputDir, "index.ts")
322
+ await writeFile(indexPath, exports.join("\n") + "\n", "utf8")
323
+ files_written.push(indexPath)
324
+ }
325
+
326
+ return {
327
+ regenerated: true,
328
+ output_dir: outputDir,
329
+ files_written,
330
+ endpoints_count: endpoints.length,
331
+ skipped: skipped.length ? skipped : undefined,
332
+ }
333
+ }
334
+
335
+ // ─── Standalone tool ────────────────────────────────────────────────────────
336
+
337
+ export const dypaiCodegenTool = {
338
+ name: "dypai_codegen",
339
+ description:
340
+ "Regenerate TypeScript types from dypai/schema.sql + dypai/endpoints/*.yaml into src/dypai/ (default). " +
341
+ "Produces: database.ts (Database interface for tables), api.ts (typed wrapper around dypai.api.*), index.ts (re-exports). " +
342
+ "Auto-runs after dypai_pull, dypai_push, and DDL via execute_sql — this tool is for manual/CI regeneration. " +
343
+ "Configure output location via dypai.config.yaml codegen.output_dir and codegen.sdk_import.",
344
+ inputSchema: {
345
+ type: "object",
346
+ properties: {
347
+ root_dir: { type: "string", default: "./dypai", description: "Path to the dypai/ folder." },
348
+ },
349
+ },
350
+ async execute({ root_dir = "./dypai" } = {}) {
351
+ const rootDir = resolvePath(process.cwd(), root_dir)
352
+ const result = await regenerateTypes(rootDir)
353
+ if (result.skipped) {
354
+ return { success: true, ...result, hint: result.reason }
355
+ }
356
+ return {
357
+ success: true,
358
+ ...result,
359
+ hint: `Import from '@/dypai' (or relative): \`import { api } from '@/dypai'\`. Types regenerate automatically on pull/push/DDL.`,
360
+ }
361
+ },
362
+ }
@@ -129,34 +129,12 @@ function detectFramework(dir) {
129
129
  } catch { return null }
130
130
  }
131
131
 
132
- export const deployTool = {
133
- name: "deploy_frontend",
134
- description: `Deploy frontend source code from a local directory.
135
-
136
- Reads all source files from the specified directory and uploads them to DYPAI.
137
- The build runs in the cloud no local Node.js or npm required.
138
-
139
- Supports: React, Vite, Next.js, Astro, SvelteKit, Nuxt, Vue, CRA, and more.
140
- DYPAI auto-detects the framework and routes to the optimal backend.
141
-
142
- After deploying, the site is live at https://{slug}.dypai.app within ~30s.`,
143
-
144
- inputSchema: {
145
- type: "object",
146
- properties: {
147
- sourceDirectory: {
148
- type: "string",
149
- description: "Absolute path to the project source directory (e.g. /Users/me/my-app). Must contain a package.json.",
150
- },
151
- project_id: {
152
- type: "string",
153
- description: "Project UUID. Required.",
154
- },
155
- },
156
- required: ["sourceDirectory", "project_id"],
157
- },
158
-
159
- async execute({ sourceDirectory, project_id }) {
132
+ /**
133
+ * Pure deploy function — used by the consolidated `manage_frontend` tool.
134
+ * Reads the source directory, uploads to the API, returns a structured
135
+ * response the caller can hand back to the agent.
136
+ */
137
+ export async function deployFromSource({ sourceDirectory, project_id }) {
160
138
  if (!existsSync(sourceDirectory)) {
161
139
  return { error: `Directory not found: ${sourceDirectory}` }
162
140
  }
@@ -201,8 +179,8 @@ After deploying, the site is live at https://{slug}.dypai.app within ~30s.`,
201
179
  build_status: "queued",
202
180
  next_step: {
203
181
  action: "poll_build_status",
204
- tool: "get_build_status",
205
- arg: { project_id },
182
+ tool: "manage_frontend",
183
+ arg: { operation: "build_status", project_id },
206
184
  interval_seconds: 5,
207
185
  expected_total_seconds: 60,
208
186
  terminal_statuses: ["success", "failure"],
@@ -210,11 +188,10 @@ After deploying, the site is live at https://{slug}.dypai.app within ~30s.`,
210
188
  message:
211
189
  `Deploy accepted — ${files.length} files (${Math.round(total / 1024)} KB) queued. ` +
212
190
  `The build is running in the cloud (${label}, typically 20–60s). ` +
213
- `Call get_build_status with project_id="${project_id}" every ~5s until status is "success" or "failure" ` +
191
+ `Call manage_frontend({operation:"build_status", project_id:"${project_id}"}) every ~5s until status is "success" or "failure" ` +
214
192
  `before reporting completion to the user. The site is NOT live yet.${skippedMsg}`,
215
193
  }
216
194
  } catch (e) {
217
195
  return { error: `Deploy failed: ${e.message}` }
218
196
  }
219
- },
220
197
  }
@@ -1,73 +1,70 @@
1
1
  /**
2
- * Domain management tools add, list, remove custom domains.
2
+ * manage_domain single entry point for custom-domain operations.
3
+ *
4
+ * Consolidates what used to be add_domain / list_domains / remove_domain into
5
+ * one `operation`-dispatched tool (same pattern as manage_users / manage_roles).
6
+ * `verify` is also included so the agent can trigger an on-demand DNS/SSL
7
+ * check after the user configures their CNAME.
3
8
  */
4
9
 
5
10
  import { api } from "../api.js"
6
11
 
7
- export const addDomainTool = {
8
- name: "add_domain",
9
- description: `Add a custom domain to your project's frontend.
10
-
11
- Returns DNS instructions (CNAME record) that the user needs to configure
12
- at their domain registrar. SSL is automatic once DNS propagates.`,
12
+ export const manageDomainTool = {
13
+ name: "manage_domain",
14
+ description:
15
+ "Manage custom domains for the project's frontend. " +
16
+ "Operations: list (all domains + DNS/SSL status), add (returns the CNAME the user must configure at their registrar — SSL is automatic), " +
17
+ "remove (disconnect a domain), verify (force an on-demand DNS/SSL re-check for a domain). " +
18
+ "The platform polls DNS automatically in the background, so `verify` is only needed when the user just updated their DNS and wants immediate feedback.",
13
19
 
14
20
  inputSchema: {
15
21
  type: "object",
16
22
  properties: {
17
- project_id: { type: "string", description: "Project UUID." },
18
- domain: { type: "string", description: "Domain to add (e.g. www.example.com)" },
23
+ operation: {
24
+ type: "string",
25
+ enum: ["list", "add", "remove", "verify"],
26
+ description: "Which action to run.",
27
+ },
28
+ project_id: {
29
+ type: "string",
30
+ description: "Project UUID. Auto-injected from dypai.config.yaml when omitted.",
31
+ },
32
+ domain: {
33
+ type: "string",
34
+ description: "Domain name (required for add, remove, verify). E.g. 'www.example.com'.",
35
+ },
19
36
  },
20
- required: ["project_id", "domain"],
37
+ required: ["operation"],
21
38
  },
22
39
 
23
- async execute({ project_id, domain }) {
24
- try {
25
- return await api.post(`/api/engine/${project_id}/frontend/domains`, { domain })
26
- } catch (e) {
27
- return { error: e.message }
40
+ async execute({ operation, project_id, domain } = {}) {
41
+ if (!operation) {
42
+ return { success: false, error: "operation is required (list | add | remove | verify)." }
28
43
  }
29
- },
30
- }
31
-
32
- export const listDomainsTool = {
33
- name: "list_domains",
34
- description: "List all custom domains configured for the project's frontend.",
35
-
36
- inputSchema: {
37
- type: "object",
38
- properties: {
39
- project_id: { type: "string", description: "Project UUID." },
40
- },
41
- required: ["project_id"],
42
- },
43
-
44
- async execute({ project_id }) {
45
- try {
46
- return await api.get(`/api/engine/${project_id}/frontend/domains`)
47
- } catch (e) {
48
- return { error: e.message }
44
+ if (!project_id) {
45
+ return { success: false, error: "project_id is required. Set it in dypai.config.yaml or pass it explicitly." }
49
46
  }
50
- },
51
- }
52
-
53
- export const removeDomainTool = {
54
- name: "remove_domain",
55
- description: "Remove a custom domain from the project's frontend.",
56
47
 
57
- inputSchema: {
58
- type: "object",
59
- properties: {
60
- project_id: { type: "string", description: "Project UUID." },
61
- domain: { type: "string", description: "Domain to remove." },
62
- },
63
- required: ["project_id", "domain"],
64
- },
48
+ const needsDomain = ["add", "remove", "verify"]
49
+ if (needsDomain.includes(operation) && !domain) {
50
+ return { success: false, error: `operation '${operation}' requires a 'domain' argument.` }
51
+ }
65
52
 
66
- async execute({ project_id, domain }) {
67
53
  try {
68
- return await api.delete(`/api/engine/${project_id}/frontend/domains/${domain}`)
54
+ switch (operation) {
55
+ case "list":
56
+ return await api.get(`/api/engine/${project_id}/frontend/domains`)
57
+ case "add":
58
+ return await api.post(`/api/engine/${project_id}/frontend/domains`, { domain })
59
+ case "remove":
60
+ return await api.delete(`/api/engine/${project_id}/frontend/domains/${encodeURIComponent(domain)}`)
61
+ case "verify":
62
+ return await api.get(`/api/engine/${project_id}/frontend/domains/${encodeURIComponent(domain)}/verify`)
63
+ default:
64
+ return { success: false, error: `Unknown operation '${operation}'. Use list | add | remove | verify.` }
65
+ }
69
66
  } catch (e) {
70
- return { error: e.message }
67
+ return { success: false, error: e.message, operation, domain: domain || null }
71
68
  }
72
69
  },
73
70
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Client-side enrichment pass-through.
3
+ *
4
+ * Response enrichment (hints on errors, next_step on non-obvious success) lives
5
+ * on the REMOTE MCP server now (dypai-mcp, Python) so every client — local
6
+ * MCP, dashboard, direct curl — gets the same agent-UX treatment. Keeping
7
+ * this file as a no-op stub lets us re-introduce client-only enrichment
8
+ * later (e.g. for local filesystem context) without touching index.js.
9
+ */
10
+
11
+ export function enrichSuccess(_toolName, result) {
12
+ return result
13
+ }
14
+
15
+ export function enrichError(_toolName, error) {
16
+ return error
17
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * manage_frontend — single entry point for the frontend deploy lifecycle.
3
+ *
4
+ * Consolidates what used to be five separate tools:
5
+ * - deploy_frontend → operation: deploy
6
+ * - get_frontend_status → operation: status
7
+ * - get_build_status → operation: build_status
8
+ * - list_deployments → operation: list_deployments
9
+ * - get_deployment_logs → operation: logs
10
+ *
11
+ * Same pattern as manage_users / manage_roles / manage_domain. Cuts 5 tools to 1.
12
+ */
13
+
14
+ import { api } from "../api.js"
15
+ import { deployFromSource } from "./deploy.js"
16
+
17
+ export const manageFrontendTool = {
18
+ name: "manage_frontend",
19
+ description:
20
+ "Manage the project's frontend deploy lifecycle. Operations:\n" +
21
+ " - deploy: upload source files from a local directory and queue a build. Returns immediately with build_status=\"queued\" — poll with build_status until \"success\" or \"failure\".\n" +
22
+ " - status: current live deploy info (URL, last deploy time, size).\n" +
23
+ " - build_status: progress of the current/latest build (queued/building/success/failure + stage + %).\n" +
24
+ " - list_deployments: recent deploy history (status, commit, duration, URL).\n" +
25
+ " - logs: build logs for a specific deployment (needs deployment_id from list_deployments).",
26
+
27
+ inputSchema: {
28
+ type: "object",
29
+ properties: {
30
+ operation: {
31
+ type: "string",
32
+ enum: ["deploy", "status", "build_status", "list_deployments", "logs"],
33
+ description: "Which action to run.",
34
+ },
35
+ project_id: {
36
+ type: "string",
37
+ description: "Project UUID. Auto-injected from dypai.config.yaml when omitted (except `deploy`, which needs it explicit).",
38
+ },
39
+ sourceDirectory: {
40
+ type: "string",
41
+ description: "deploy only. Absolute path to the project source directory (must contain package.json).",
42
+ },
43
+ deployment_id: {
44
+ type: "string",
45
+ description: "logs only. Deployment UUID obtained from list_deployments.",
46
+ },
47
+ limit: {
48
+ type: "number",
49
+ description: "list_deployments only. Max deployments to return (default 10, max 20).",
50
+ },
51
+ },
52
+ required: ["operation"],
53
+ },
54
+
55
+ async execute({ operation, project_id, sourceDirectory, deployment_id, limit } = {}) {
56
+ if (!operation) {
57
+ return { success: false, error: "operation is required (deploy | status | build_status | list_deployments | logs)." }
58
+ }
59
+ if (!project_id) {
60
+ return { success: false, error: "project_id is required. Set it in dypai.config.yaml or pass it explicitly." }
61
+ }
62
+
63
+ try {
64
+ switch (operation) {
65
+ case "deploy":
66
+ if (!sourceDirectory) {
67
+ return { success: false, error: "operation 'deploy' requires 'sourceDirectory' (absolute path to your frontend project root)." }
68
+ }
69
+ return await deployFromSource({ sourceDirectory, project_id })
70
+
71
+ case "status":
72
+ return await api.get(`/api/engine/${project_id}/frontend`)
73
+
74
+ case "build_status":
75
+ return await api.get(`/api/engine/${project_id}/frontend/build-status`)
76
+
77
+ case "list_deployments":
78
+ return await api.get(`/api/engine/${project_id}/frontend/deployments?limit=${limit || 10}`)
79
+
80
+ case "logs":
81
+ if (!deployment_id) {
82
+ return { success: false, error: "operation 'logs' requires 'deployment_id'. Call list_deployments first to get the ID." }
83
+ }
84
+ return await api.get(`/api/engine/${project_id}/frontend/deployments/${deployment_id}/logs`)
85
+
86
+ default:
87
+ return { success: false, error: `Unknown operation '${operation}'. Use deploy | status | build_status | list_deployments | logs.` }
88
+ }
89
+ } catch (e) {
90
+ return { success: false, error: e.message, operation }
91
+ }
92
+ },
93
+ }