@dypai-ai/mcp 1.0.8 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dypai-ai/mcp",
3
- "version": "1.0.8",
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",
@@ -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 = [