@dypai-ai/mcp 1.0.7 → 1.0.9
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 +2 -3
- package/src/auto-update.js +198 -0
- package/src/index.js +17 -6
- package/src/tools/deploy.js +5 -8
- package/src/tools/scaffold.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dypai-ai/mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "DYPAI MCP Server — AI agent toolkit for building and deploying full-stack apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -17,8 +17,7 @@
|
|
|
17
17
|
"deploy",
|
|
18
18
|
"backend",
|
|
19
19
|
"database",
|
|
20
|
-
"api"
|
|
21
|
-
"cloudflare-pages"
|
|
20
|
+
"api"
|
|
22
21
|
],
|
|
23
22
|
"license": "MIT",
|
|
24
23
|
"repository": {
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-update for @dypai-ai/mcp.
|
|
3
|
+
*
|
|
4
|
+
* On startup, checks the npm registry for a newer version. If found, performs
|
|
5
|
+
* the appropriate update (clear npx cache or `npm install -g`) and exits with
|
|
6
|
+
* code 0 so the host (Cursor / Claude / Trae / VSCode) re-spawns the process
|
|
7
|
+
* with the freshly installed version.
|
|
8
|
+
*
|
|
9
|
+
* Throttled to one check every 6h to avoid hammering the registry.
|
|
10
|
+
*
|
|
11
|
+
* Disable with: DYPAI_NO_AUTOUPDATE=1
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync, rmSync, readdirSync, statSync } from "node:fs";
|
|
15
|
+
import { join, dirname } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { homedir, tmpdir } from "node:os";
|
|
18
|
+
import { execSync } from "node:child_process";
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const PKG_PATH = join(__dirname, "..", "package.json");
|
|
22
|
+
const PKG_NAME = "@dypai-ai/mcp";
|
|
23
|
+
const REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
|
|
24
|
+
const CHECK_TIMEOUT_MS = 2000;
|
|
25
|
+
const THROTTLE_HOURS = 6;
|
|
26
|
+
const STATE_FILE = join(tmpdir(), "dypai-mcp-update-state.json");
|
|
27
|
+
|
|
28
|
+
function log(msg) {
|
|
29
|
+
// stderr — stdout is reserved for the MCP protocol
|
|
30
|
+
console.error(`[dypai-mcp:auto-update] ${msg}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readState() {
|
|
34
|
+
try { return JSON.parse(readFileSync(STATE_FILE, "utf-8")); } catch { return {}; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeState(state) {
|
|
38
|
+
try { writeFileSync(STATE_FILE, JSON.stringify(state)); } catch {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getCurrentVersion() {
|
|
42
|
+
try { return JSON.parse(readFileSync(PKG_PATH, "utf-8")).version; }
|
|
43
|
+
catch { return null; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** semver compare: returns 1 if a > b, -1 if a < b, 0 if equal. */
|
|
47
|
+
function compareVersions(a, b) {
|
|
48
|
+
const pa = String(a).split(".").map((n) => parseInt(n, 10) || 0);
|
|
49
|
+
const pb = String(b).split(".").map((n) => parseInt(n, 10) || 0);
|
|
50
|
+
for (let i = 0; i < 3; i += 1) {
|
|
51
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
52
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
53
|
+
}
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function fetchLatestManifest() {
|
|
58
|
+
const ctrl = new AbortController();
|
|
59
|
+
const timer = setTimeout(() => ctrl.abort(), CHECK_TIMEOUT_MS);
|
|
60
|
+
try {
|
|
61
|
+
const res = await fetch(REGISTRY_URL, { signal: ctrl.signal });
|
|
62
|
+
if (!res.ok) return null;
|
|
63
|
+
return await res.json();
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
} finally {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* After npm publish there's a 30s–2min window where the registry knows the
|
|
73
|
+
* version but the tarball is not yet replicated across the CDN. If we clear
|
|
74
|
+
* the cache and exit during that window, the next spawn fails to download
|
|
75
|
+
* and the MCP becomes unusable.
|
|
76
|
+
*
|
|
77
|
+
* This does a HEAD on the tarball URL with a small timeout. Returns true only
|
|
78
|
+
* if the artifact is actually retrievable.
|
|
79
|
+
*/
|
|
80
|
+
async function isTarballReady(tarballUrl) {
|
|
81
|
+
if (!tarballUrl) return false;
|
|
82
|
+
const ctrl = new AbortController();
|
|
83
|
+
const timer = setTimeout(() => ctrl.abort(), CHECK_TIMEOUT_MS);
|
|
84
|
+
try {
|
|
85
|
+
const res = await fetch(tarballUrl, { method: "HEAD", signal: ctrl.signal });
|
|
86
|
+
return res.ok;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
} finally {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Detect how this process is running so we know how to update. */
|
|
95
|
+
function detectInstallMethod() {
|
|
96
|
+
const scriptPath = process.argv[1] || "";
|
|
97
|
+
if (scriptPath.includes("/_npx/")) return "npx";
|
|
98
|
+
if (scriptPath.includes("/node_modules/") && scriptPath.includes("/.bin/")) return "global";
|
|
99
|
+
// global installs typically resolve to /usr/local/lib/... on mac/linux or %APPDATA%/npm on windows
|
|
100
|
+
if (scriptPath.includes("/lib/node_modules/")) return "global";
|
|
101
|
+
if (scriptPath.match(/[\\/]npm[\\/]node_modules[\\/]/)) return "global";
|
|
102
|
+
return "unknown";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Wipe ONLY this package's cache from the user's npx cache directory. */
|
|
106
|
+
function clearNpxCacheForPackage() {
|
|
107
|
+
const npxRoot = join(homedir(), ".npm", "_npx");
|
|
108
|
+
if (!existsSync(npxRoot)) return false;
|
|
109
|
+
|
|
110
|
+
let cleared = 0;
|
|
111
|
+
for (const dir of readdirSync(npxRoot)) {
|
|
112
|
+
const fullPath = join(npxRoot, dir);
|
|
113
|
+
try {
|
|
114
|
+
if (!statSync(fullPath).isDirectory()) continue;
|
|
115
|
+
const pkgManifest = join(fullPath, "node_modules", PKG_NAME, "package.json");
|
|
116
|
+
if (existsSync(pkgManifest)) {
|
|
117
|
+
rmSync(fullPath, { recursive: true, force: true });
|
|
118
|
+
cleared += 1;
|
|
119
|
+
}
|
|
120
|
+
} catch { /* ignore */ }
|
|
121
|
+
}
|
|
122
|
+
return cleared > 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function installGlobalLatest() {
|
|
126
|
+
try {
|
|
127
|
+
execSync(`npm install -g ${PKG_NAME}@latest`, {
|
|
128
|
+
stdio: "pipe",
|
|
129
|
+
timeout: 60_000,
|
|
130
|
+
});
|
|
131
|
+
return true;
|
|
132
|
+
} catch (e) {
|
|
133
|
+
log(`global install failed: ${e.message?.slice(0, 200)}`);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Main entry point — call once at startup.
|
|
140
|
+
* Returns quickly. Exits the process if an update was applied.
|
|
141
|
+
*/
|
|
142
|
+
export async function checkForUpdates({ force = false } = {}) {
|
|
143
|
+
if (process.env.DYPAI_NO_AUTOUPDATE === "1") return { skipped: "disabled" };
|
|
144
|
+
|
|
145
|
+
const current = getCurrentVersion();
|
|
146
|
+
if (!current) return { skipped: "no current version" };
|
|
147
|
+
|
|
148
|
+
// Throttle
|
|
149
|
+
if (!force) {
|
|
150
|
+
const state = readState();
|
|
151
|
+
if (state.lastCheckAt) {
|
|
152
|
+
const hoursAgo = (Date.now() - state.lastCheckAt) / 3_600_000;
|
|
153
|
+
if (hoursAgo < THROTTLE_HOURS) return { skipped: "throttled" };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const manifest = await fetchLatestManifest();
|
|
158
|
+
const latest = manifest?.version || null;
|
|
159
|
+
const tarballUrl = manifest?.dist?.tarball || null;
|
|
160
|
+
writeState({ lastCheckAt: Date.now(), lastSeenLatest: latest, current });
|
|
161
|
+
|
|
162
|
+
if (!latest) return { skipped: "registry unreachable" };
|
|
163
|
+
if (compareVersions(latest, current) <= 0) return { uptodate: true, current };
|
|
164
|
+
|
|
165
|
+
log(`update available: ${current} → ${latest}`);
|
|
166
|
+
|
|
167
|
+
// Critical safety: don't clear the cache until the new tarball is actually
|
|
168
|
+
// downloadable. Right after `npm publish` the version is visible in the
|
|
169
|
+
// registry metadata up to ~2 min before the tarball replicates to all CDNs.
|
|
170
|
+
// If we clear the cache + exit during that window, next spawn fails.
|
|
171
|
+
const ready = await isTarballReady(tarballUrl);
|
|
172
|
+
if (!ready) {
|
|
173
|
+
log(`new version ${latest} not yet downloadable from CDN; will retry later`);
|
|
174
|
+
return { skipped: "tarball not ready" };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const method = detectInstallMethod();
|
|
178
|
+
let updated = false;
|
|
179
|
+
|
|
180
|
+
if (method === "npx") {
|
|
181
|
+
log(`tarball verified — clearing npx cache so the next spawn pulls ${PKG_NAME}@${latest}`);
|
|
182
|
+
updated = clearNpxCacheForPackage();
|
|
183
|
+
} else if (method === "global") {
|
|
184
|
+
log(`tarball verified — running: npm install -g ${PKG_NAME}@latest`);
|
|
185
|
+
updated = installGlobalLatest();
|
|
186
|
+
} else {
|
|
187
|
+
log(`unknown install method (script=${process.argv[1] || "?"}); skipping self-update`);
|
|
188
|
+
return { skipped: "unknown install method" };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!updated) {
|
|
192
|
+
log("update step did not succeed; continuing with current version");
|
|
193
|
+
return { skipped: "update failed" };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
log(`updated to ${latest}. Exiting so the IDE re-spawns the MCP with the new version.`);
|
|
197
|
+
process.exit(0);
|
|
198
|
+
}
|
package/src/index.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { createInterface } from "readline"
|
|
23
|
+
import { checkForUpdates } from "./auto-update.js"
|
|
23
24
|
import { deployTool } from "./tools/deploy.js"
|
|
24
25
|
import { scaffoldTool } from "./tools/scaffold.js"
|
|
25
26
|
import { addDomainTool, listDomainsTool, removeDomainTool } from "./tools/domains.js"
|
|
@@ -27,6 +28,13 @@ import { frontendStatusTool, buildStatusTool, listDeploymentsTool, getDeployment
|
|
|
27
28
|
import { bulkUpsertTool } from "./tools/bulk-upsert.js"
|
|
28
29
|
import { proxyToolCall } from "./tools/proxy.js"
|
|
29
30
|
|
|
31
|
+
// ── Self-update ─────────────────────────────────────────────────────────────
|
|
32
|
+
// Throttled (6h) check against the npm registry. If a newer version is
|
|
33
|
+
// available, the update is performed and the process exits cleanly so the
|
|
34
|
+
// IDE re-spawns it with the latest. Disable with DYPAI_NO_AUTOUPDATE=1.
|
|
35
|
+
// Network failures are silently ignored — never blocks startup more than ~2s.
|
|
36
|
+
await checkForUpdates().catch(() => {})
|
|
37
|
+
|
|
30
38
|
// ── Local tools (filesystem access) ─────────────────────────────────────────
|
|
31
39
|
|
|
32
40
|
const LOCAL_TOOLS = [
|
|
@@ -65,7 +73,8 @@ const FALLBACK_REMOTE_TOOLS = [
|
|
|
65
73
|
{ name: "get_auth_users", description: "List users.", inputSchema: { type: "object", properties: { project_id: { type: "string" } }, required: [] } },
|
|
66
74
|
{ name: "list_buckets", description: "List storage buckets.", inputSchema: { type: "object", properties: { project_id: { type: "string" } }, required: [] } },
|
|
67
75
|
{ name: "search_docs", description: "Search documentation.", inputSchema: { type: "object", properties: { query: { type: "string" } }, required: ["query"] } },
|
|
68
|
-
{ name: "
|
|
76
|
+
{ name: "search_workflow_templates", description: "Search workflow templates.", inputSchema: { type: "object", properties: { query: { type: "string" } }, required: ["query"] } },
|
|
77
|
+
{ name: "search_project_templates", description: "Search project starter templates.", inputSchema: { type: "object", properties: { query: { type: "string" } }, required: ["query"] } },
|
|
69
78
|
{ name: "search_nodes", description: "Search workflow nodes.", inputSchema: { type: "object", properties: { query: { type: "string" } }, required: ["query"] } },
|
|
70
79
|
]
|
|
71
80
|
|
|
@@ -74,7 +83,7 @@ const REMOTE_TOOLS = [
|
|
|
74
83
|
// ── Project ───────────────────────────────────────────────────────────────
|
|
75
84
|
{ name: "list_projects", description: "Lists all projects you have access to across your organizations. Returns project id, name, description, organization, subscription plan, and status. Use this as the first step to discover which projects are available, then pass project_id to other tools.", inputSchema: { type: "object", properties: { organization_id: { type: "string", description: "Optional. Filter projects by organization UUID." } }, required: [] } },
|
|
76
85
|
{ name: "get_project", description: "Gets detailed information about a specific project. Returns project name, description, organization, plan, status, engine URL, frontend slug, and timestamps.", inputSchema: { type: "object", properties: { project_id: { type: "string" } }, required: ["project_id"] } },
|
|
77
|
-
{ name: "create_project", description: "Create a new DYPAI project (free plan). Creates a full project with database, API engine, GitHub repo, and frontend hosting. Provisioning takes ~1 minute.", inputSchema: { type: "object", properties: { name: { type: "string", description: "Project name (e.g. 'My Veterinary App')" }, organization_id: { type: "string", description: "Optional. Uses default org if omitted." }, description: { type: "string" }, template_slug: { type: "string", description: "Optional. Start from a template (e.g. 'clinic', 'gym'). Use
|
|
86
|
+
{ name: "create_project", description: "Create a new DYPAI project (free plan). Creates a full project with database, API engine, GitHub repo, and frontend hosting. Provisioning takes ~1 minute.", inputSchema: { type: "object", properties: { name: { type: "string", description: "Project name (e.g. 'My Veterinary App')" }, organization_id: { type: "string", description: "Optional. Uses default org if omitted." }, description: { type: "string" }, template_slug: { type: "string", description: "Optional. Start from a project template (e.g. 'clinic', 'gym', 'blank'). Use search_project_templates to browse." } }, required: ["name"] } },
|
|
78
87
|
{ name: "get_app_credentials", description: "Lists available credentials in the current application. Returns API keys, anon key, service role key, and engine URL needed for SDK configuration.", inputSchema: { type: "object", properties: { project_id: { type: "string" } }, required: [] } },
|
|
79
88
|
|
|
80
89
|
// ── Database ──────────────────────────────────────────────────────────────
|
|
@@ -105,7 +114,8 @@ const REMOTE_TOOLS = [
|
|
|
105
114
|
|
|
106
115
|
// ── Knowledge ─────────────────────────────────────────────────────────────
|
|
107
116
|
{ name: "search_docs", description: "Search DYPAI documentation. Use this when unsure about SDK usage, auth patterns, workflow nodes, or platform features. Returns relevant documentation chunks.", inputSchema: { type: "object", properties: { query: { type: "string", description: "What you want to learn about" } }, required: ["query"] } },
|
|
108
|
-
{ name: "
|
|
117
|
+
{ name: "search_workflow_templates", description: "Search workflow templates by description. Returns ready-to-use workflow code for common patterns: CRUD operations, payment gateways, email sending, AI chatbots, data pipelines, etc.", inputSchema: { type: "object", properties: { query: { type: "string", description: "What the workflow should do (e.g. 'send email', 'stripe payment')" }, category: { type: "string", description: "Optional: AI, Database, Payments, Communication, Logic, Storage" } }, required: ["query"] } },
|
|
118
|
+
{ name: "search_project_templates", description: "Search project starter templates by description. Returns template metadata and slugs for starters like clinic, gym, waitlist, blank, auth, or landing.", inputSchema: { type: "object", properties: { query: { type: "string", description: "What kind of project starter you need (e.g. 'gym app', 'landing page', 'auth starter')" }, category: { type: "string", description: "Optional category filter" } }, required: ["query"] } },
|
|
109
119
|
]
|
|
110
120
|
|
|
111
121
|
// ── Server Instructions ──────────────────────────────────────────────────────
|
|
@@ -122,9 +132,10 @@ const SERVER_INSTRUCTIONS = `You are building full-stack applications on the DYP
|
|
|
122
132
|
1. Create tables with execute_sql (check get_app_tables first to avoid duplicates)
|
|
123
133
|
2. Create endpoints with create_endpoint using workflow_code
|
|
124
134
|
3. Test with test_workflow immediately after creating/updating
|
|
125
|
-
4. Use
|
|
126
|
-
5. Use
|
|
127
|
-
6. Use
|
|
135
|
+
4. Use search_workflow_templates to find ready-made workflow patterns
|
|
136
|
+
5. Use search_project_templates when the user wants a starter app or template-based project
|
|
137
|
+
6. Use search_nodes to discover available node types
|
|
138
|
+
7. Use search_docs when unsure about patterns
|
|
128
139
|
|
|
129
140
|
## Build Frontend
|
|
130
141
|
- SDK client is already configured at src/lib/dypai.ts — just import it:
|
package/src/tools/deploy.js
CHANGED
|
@@ -133,13 +133,13 @@ export const deployTool = {
|
|
|
133
133
|
name: "deploy_frontend",
|
|
134
134
|
description: `Deploy frontend source code from a local directory.
|
|
135
135
|
|
|
136
|
-
Reads all source files from the specified directory
|
|
137
|
-
and Cloudflare Pages automatically builds and deploys the project.
|
|
138
|
-
|
|
139
|
-
Supports: React, Vite, Next.js, Astro, SvelteKit, Nuxt, CRA, and more.
|
|
136
|
+
Reads all source files from the specified directory and uploads them to DYPAI.
|
|
140
137
|
The build runs in the cloud — no local Node.js or npm required.
|
|
141
138
|
|
|
142
|
-
|
|
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
143
|
|
|
144
144
|
inputSchema: {
|
|
145
145
|
type: "object",
|
|
@@ -227,7 +227,6 @@ After deploying, the site is live at https://{slug}.dypai.app within ~1 minute.`
|
|
|
227
227
|
files_pushed: files.length,
|
|
228
228
|
size_bytes: total,
|
|
229
229
|
framework: label,
|
|
230
|
-
build: "cloudflare_pages",
|
|
231
230
|
build_status: "building",
|
|
232
231
|
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
232
|
}
|
|
@@ -239,7 +238,6 @@ After deploying, the site is live at https://{slug}.dypai.app within ~1 minute.`
|
|
|
239
238
|
url: result.url,
|
|
240
239
|
files_pushed: files.length,
|
|
241
240
|
framework: label,
|
|
242
|
-
build: "cloudflare_pages",
|
|
243
241
|
build_status: "failure",
|
|
244
242
|
build_error: buildResult.error,
|
|
245
243
|
message: `Deploy pushed ${files.length} files but the ${label} build FAILED. Build error:\n${buildResult.error}`,
|
|
@@ -258,7 +256,6 @@ After deploying, the site is live at https://{slug}.dypai.app within ~1 minute.`
|
|
|
258
256
|
skipped_files: skipped.length > 0 ? skipped : undefined,
|
|
259
257
|
size_bytes: total,
|
|
260
258
|
framework: label,
|
|
261
|
-
build: "cloudflare_pages",
|
|
262
259
|
build_status: "success",
|
|
263
260
|
build_duration: buildResult.duration,
|
|
264
261
|
message: `Deployed ${files.length} files (${Math.round(total / 1024)} KB). ${label} build succeeded${buildResult.duration ? ` in ${buildResult.duration}s` : ""}. Live at ${result.url}${skippedMsg}`,
|
package/src/tools/scaffold.js
CHANGED
|
@@ -19,7 +19,7 @@ Scaffolds a project directory with:
|
|
|
19
19
|
- MCP config for your IDE
|
|
20
20
|
- .env with engine URL
|
|
21
21
|
|
|
22
|
-
Use
|
|
22
|
+
Use search_project_templates first to find available templates, then pass the template slug here.
|
|
23
23
|
Or use "blank" for an empty starter project.`,
|
|
24
24
|
|
|
25
25
|
inputSchema: {
|
|
@@ -35,7 +35,7 @@ Or use "blank" for an empty starter project.`,
|
|
|
35
35
|
},
|
|
36
36
|
template: {
|
|
37
37
|
type: "string",
|
|
38
|
-
description: 'Template slug (e.g. "clinic", "gym", "blank"). Use
|
|
38
|
+
description: 'Template slug (e.g. "clinic", "gym", "blank"). Use search_project_templates to find available templates.',
|
|
39
39
|
default: "blank",
|
|
40
40
|
},
|
|
41
41
|
},
|