@offlocal/mcp 0.3.0 → 0.3.1

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/dist/detect.js CHANGED
@@ -313,6 +313,8 @@ function pickNodeFramework(deps) {
313
313
  return "remix";
314
314
  if ("astro" in deps)
315
315
  return "astro";
316
+ if ("vite" in deps)
317
+ return "vite";
316
318
  if ("fastify" in deps)
317
319
  return "fastify";
318
320
  if ("hono" in deps)
@@ -321,8 +323,6 @@ function pickNodeFramework(deps) {
321
323
  return "express";
322
324
  if ("koa" in deps)
323
325
  return "koa";
324
- if ("vite" in deps)
325
- return "vite";
326
326
  return "node";
327
327
  }
328
328
  function nodePort(framework) {
@@ -72,13 +72,17 @@ export async function deployHandler(input) {
72
72
  `| ${ICON.spark} Framework | ${summary.framework} (${summary.buildSource}) |\n` +
73
73
  `| Port | ${summary.port} |\n` +
74
74
  `| Bundle | ${summary.bundle.fileCount} files · ${summary.bundle.bytes} bytes |\n\n` +
75
+ (bundle.warnings.length
76
+ ? bundle.warnings.map((w) => `${ICON.warn} ${w}`).join("\n") + "\n\n"
77
+ : "") +
75
78
  `———\n` +
76
- `${ICON.arrow} **Next (you, the agent):** tell the user it's building (2–4 min) and you'll ` +
79
+ `${ICON.arrow} **Next (you, the agent):** tell the user it's building (usually 2–8 min) and you'll ` +
77
80
  `report back when it's live. Then:\n` +
78
81
  ` 1. Sleep 45 seconds.\n` +
79
82
  ` 2. \`offlocal_status({ deploymentId: "${summary.deploymentId}" })\`.\n` +
80
83
  ` 3. Sleep the \`pollAfterSeconds\` it returns, then call status again — exactly once per interval.\n` +
81
- ` 4. Repeat until \`live\` or \`failed\`. Polling faster does NOT speed up the build.\n` +
84
+ ` 4. Repeat until \`live\` or \`failed\`. **Slow is normal don't panic or declare failure on a slow deploy.** ` +
85
+ `The status tool tells you if it's genuinely stuck; only then should you stop and suggest a retry.\n` +
82
86
  `Keep the **projectId** from the live status for any database step.`,
83
87
  },
84
88
  ],
@@ -110,7 +110,10 @@ export async function connectHandler(input) {
110
110
  };
111
111
  }
112
112
  export const ProvisionInputShape = {
113
- provider: z.string().min(1).describe("The connected provider to provision on."),
113
+ provider: z
114
+ .string()
115
+ .min(1)
116
+ .describe("The CONNECTED external provider to provision on: supabase, planetscale, digitalocean, or railway. For Offlocal's own managed Postgres, use offlocal_create_managed_database instead — not this."),
114
117
  type: z
115
118
  .enum(["project", "database", "deployment", "env_vars", "branch"])
116
119
  .describe("The kind of resource to create."),
@@ -131,6 +134,11 @@ export const ProvisionInputShape = {
131
134
  };
132
135
  export async function provisionHandler(input) {
133
136
  const { provider, confirm, projectId, ...spec } = input;
137
+ if (["offlocal", "offlocal.ai"].includes(provider.toLowerCase())) {
138
+ return text(`${ICON.warn} "offlocal" isn't a provision provider. For an Offlocal Managed Postgres database, call ` +
139
+ `\`offlocal_create_managed_database({ projectId })\`. \`offlocal_provision\` is only for connected external ` +
140
+ `providers: **supabase, planetscale, digitalocean, railway**.`);
141
+ }
134
142
  const res = await provisionResource(provider, spec, {
135
143
  confirm: confirm ?? false,
136
144
  projectId,
@@ -24,6 +24,9 @@ export async function statusHandler(input) {
24
24
  const isTerminal = dep.status === "live" || dep.status === "failed";
25
25
  const pollAfterSeconds = isTerminal ? 0 : pollIntervalFor(dep.status);
26
26
  const etaSeconds = isTerminal ? 0 : etaFor(dep.status);
27
+ const secondsSinceUpdate = Math.max(0, Math.round((Date.now() - new Date(dep.updatedAt).getTime()) / 1000));
28
+ const STUCK_AFTER = 9 * 60;
29
+ const stalled = !isTerminal && secondsSinceUpdate > STUCK_AFTER;
27
30
  const summary = {
28
31
  deploymentId: dep.id,
29
32
  projectId: dep.projectId,
@@ -32,6 +35,8 @@ export async function statusHandler(input) {
32
35
  isTerminal,
33
36
  pollAfterSeconds,
34
37
  etaSeconds,
38
+ secondsSinceUpdate,
39
+ stalled,
35
40
  liveUrl: dep.liveUrl,
36
41
  errorType: dep.errorType,
37
42
  errorMessage: dep.errorMessage,
@@ -61,9 +66,19 @@ export async function statusHandler(input) {
61
66
  lines.push("");
62
67
  lines.push(bar);
63
68
  lines.push("");
64
- lines.push(`Still in progress (typical end-to-end deploy: 2–4 min). Do NOT call this tool ` +
65
- `again for at least ${pollAfterSeconds}s polling doesn't speed it up. ` +
66
- `Sleep ${pollAfterSeconds}s, then call it exactly once.`);
69
+ if (stalled) {
70
+ lines.push(`${ICON.warn} It's been at "${phase}" for ~${Math.round(secondsSinceUpdate / 60)} min with no change — ` +
71
+ `longer than a normal deploy. It may be stuck.\n` +
72
+ `Tell the user it's taking unusually long. You can wait a little longer, but do NOT keep polling indefinitely — ` +
73
+ `if it doesn't move within another couple of minutes, suggest re-running \`offlocal_deploy\` (a retry usually clears a stuck deploy).`);
74
+ }
75
+ else {
76
+ lines.push(`Building / rolling out — **this is normal and can take 2–8 minutes** (occasionally a bit more). Stay calm and don't treat slowness as failure:\n` +
77
+ `• A deploy is only failed when the status is literally **failed** — nothing else means it broke.\n` +
78
+ `• "starting container" can mean the container is already healthy but not yet promoted; it'll flip to **live** shortly.\n` +
79
+ `• Empty \`offlocal_logs\` during build / before live is expected (container logs only exist once it's running) — not an error.\n` +
80
+ `Sleep ${pollAfterSeconds}s, then call this exactly once. Polling faster does not help.`);
81
+ }
67
82
  }
68
83
  return {
69
84
  content: [{ type: "text", text: lines.join("\n") }],
package/dist/zip.js CHANGED
@@ -20,13 +20,16 @@ const IGNORED_DIRS = new Set([
20
20
  ".idea",
21
21
  ".vscode",
22
22
  ]);
23
- const IGNORED_FILE_RE = /^(\.env(\..+)?|.*\.log|\.DS_Store|Thumbs\.db|.*\.tsbuildinfo)$/;
23
+ const IGNORED_FILE_RE = /^(\.env(\..+)?|\.DS_Store|Thumbs\.db)$|\.(log|tsbuildinfo|exe|dll|dylib|so|msi|dmg|pdb|class|o|obj|lib)$/i;
24
24
  const MAX_FILE_BYTES = 50 * 1024 * 1024;
25
25
  const MAX_BUNDLE_BYTES = 250 * 1024 * 1024;
26
+ const WARN_BUNDLE_BYTES = 25 * 1024 * 1024;
27
+ const WARN_FILE_BYTES = 5 * 1024 * 1024;
26
28
  export async function bundleDirectory(sourceDir) {
27
29
  const zip = new AdmZip();
28
30
  let fileCount = 0;
29
31
  let totalBytes = 0;
32
+ const bigFiles = [];
30
33
  await walk(sourceDir, async (absolute) => {
31
34
  const rel = relative(sourceDir, absolute);
32
35
  if (!rel)
@@ -42,14 +45,26 @@ export async function bundleDirectory(sourceDir) {
42
45
  zip.addFile(zipPath, content);
43
46
  fileCount++;
44
47
  totalBytes += st.size;
48
+ if (st.size > WARN_FILE_BYTES)
49
+ bigFiles.push({ rel: zipPath, bytes: st.size });
45
50
  if (totalBytes > MAX_BUNDLE_BYTES) {
46
- throw new Error(`bundle exceeds ${MAX_BUNDLE_BYTES} bytes. Trim node_modules-like outputs or add a .offlocalignore.`);
51
+ throw new Error(`bundle exceeds ${MAX_BUNDLE_BYTES} bytes. Trim node_modules-like outputs or add a .gitignore.`);
47
52
  }
48
53
  });
49
54
  if (fileCount === 0) {
50
55
  throw new Error(`no files to bundle under ${sourceDir}`);
51
56
  }
52
- return { buffer: zip.toBuffer(), fileCount, byteSize: totalBytes };
57
+ const warnings = [];
58
+ if (totalBytes > WARN_BUNDLE_BYTES) {
59
+ warnings.push(`Bundle is ${mb(totalBytes)} — large for a deploy. Add a .gitignore for build output/binaries you don't need to ship.`);
60
+ }
61
+ for (const f of bigFiles.sort((a, b) => b.bytes - a.bytes).slice(0, 3)) {
62
+ warnings.push(`Large file bundled: ${f.rel} (${mb(f.bytes)}). Gitignore it if it isn't needed at runtime.`);
63
+ }
64
+ return { buffer: zip.toBuffer(), fileCount, byteSize: totalBytes, warnings };
65
+ }
66
+ function mb(bytes) {
67
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
53
68
  }
54
69
  async function walk(root, onFile) {
55
70
  const stack = [root];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@offlocal/mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "MCP server for Offlocal - lets your AI coding agent deploy apps via offlocal_deploy, offlocal_status, offlocal_logs, offlocal_ping.",
5
5
  "keywords": [
6
6
  "mcp",