@offlocal/mcp 0.3.1 → 0.3.2

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/client.js CHANGED
@@ -1,7 +1,13 @@
1
+ import { MCP_VERSION } from "./version.js";
1
2
  const API_URL = process.env.OFFLOCAL_API_URL?.replace(/\/+$/, "") ?? "http://localhost:4000";
2
3
  const AGENT_KEY = process.env.OFFLOCAL_AGENT_KEY ?? "";
3
4
  function authHeaders() {
4
- return AGENT_KEY ? { Authorization: `Bearer ${AGENT_KEY}` } : {};
5
+ const headers = {
6
+ "x-offlocal-mcp-version": MCP_VERSION,
7
+ };
8
+ if (AGENT_KEY)
9
+ headers.Authorization = `Bearer ${AGENT_KEY}`;
10
+ return headers;
5
11
  }
6
12
  export async function pingApi() {
7
13
  if (!AGENT_KEY) {
@@ -14,6 +20,15 @@ export async function pingApi() {
14
20
  headers: { "content-type": "application/json", ...authHeaders() },
15
21
  body: "{}",
16
22
  });
23
+ if (res.status === 426) {
24
+ const body = await res.text().catch(() => "");
25
+ return {
26
+ ok: false,
27
+ mustUpdate: true,
28
+ reason: body.slice(0, 300) ||
29
+ "This Offlocal MCP version is no longer supported. Update with: npx -y @offlocal/mcp@latest",
30
+ };
31
+ }
17
32
  if (!res.ok) {
18
33
  const body = await res.text().catch(() => "");
19
34
  return {
@@ -22,7 +37,11 @@ export async function pingApi() {
22
37
  };
23
38
  }
24
39
  const data = (await res.json());
25
- return { ok: true, user: data.user };
40
+ return {
41
+ ok: true,
42
+ user: data.user,
43
+ updateAvailable: res.headers.get("x-offlocal-mcp-update"),
44
+ };
26
45
  }
27
46
  catch (err) {
28
47
  const message = err instanceof Error ? err.message : String(err);
package/dist/detect.js CHANGED
@@ -124,6 +124,7 @@ const WEB_FRAMEWORKS = new Set([
124
124
  "remix",
125
125
  "astro",
126
126
  "vite",
127
+ "cra",
127
128
  "static",
128
129
  ]);
129
130
  const API_FRAMEWORKS = new Set(["fastify", "hono", "express", "koa", "nestjs"]);
@@ -214,6 +215,8 @@ export function apiUrlEnvVar(framework) {
214
215
  return { key: "NEXT_PUBLIC_API_URL", buildTime: true };
215
216
  case "vite":
216
217
  return { key: "VITE_API_URL", buildTime: true };
218
+ case "cra":
219
+ return { key: "REACT_APP_API_URL", buildTime: true };
217
220
  case "nuxt":
218
221
  return { key: "NUXT_PUBLIC_API_URL", buildTime: true };
219
222
  case "astro":
@@ -279,9 +282,12 @@ async function detectPython(sourceDir, appName) {
279
282
  const hasManage = await fileExists(join(sourceDir, "manage.py"));
280
283
  let framework = "python";
281
284
  let startCommand = "python app.py";
285
+ const notes = [];
282
286
  if (hasManage || manifest.includes("django")) {
283
287
  framework = "django";
284
- startCommand = "python manage.py runserver 0.0.0.0:$PORT";
288
+ const wsgi = await findDjangoWsgiModule(sourceDir);
289
+ startCommand = `python manage.py migrate --noinput && gunicorn ${wsgi}:application --bind 0.0.0.0:$PORT`;
290
+ notes.push("Django runs under gunicorn (production) and migrates on start. IMPORTANT: a default project's `ALLOWED_HOSTS` is empty and will 400 every request. Make settings.py read it from the environment — e.g. `ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')` — and offlocal will set ALLOWED_HOSTS to the live host automatically. Use a real database (SQLite resets on every cold start), and run `collectstatic` in a build step if you serve static files.");
285
291
  }
286
292
  else if (manifest.includes("fastapi")) {
287
293
  framework = "fastapi";
@@ -302,8 +308,27 @@ async function detectPython(sourceDir, appName) {
302
308
  startCommand,
303
309
  port: 8000,
304
310
  strategy: "template",
311
+ notes: notes.length ? notes : undefined,
305
312
  };
306
313
  }
314
+ async function findDjangoWsgiModule(sourceDir) {
315
+ const manage = await readTextSafe(join(sourceDir, "manage.py"));
316
+ const m = /DJANGO_SETTINGS_MODULE['"]\s*,\s*['"]([\w.]+)\.settings['"]/.exec(manage);
317
+ if (m?.[1])
318
+ return `${m[1]}.wsgi`;
319
+ try {
320
+ const entries = await readdir(sourceDir, { withFileTypes: true });
321
+ for (const e of entries) {
322
+ if (e.isDirectory() &&
323
+ (await fileExists(join(sourceDir, e.name, "wsgi.py")))) {
324
+ return `${e.name}.wsgi`;
325
+ }
326
+ }
327
+ }
328
+ catch {
329
+ }
330
+ return "wsgi";
331
+ }
307
332
  function pickNodeFramework(deps) {
308
333
  if ("next" in deps)
309
334
  return "nextjs";
@@ -315,6 +340,8 @@ function pickNodeFramework(deps) {
315
340
  return "astro";
316
341
  if ("vite" in deps)
317
342
  return "vite";
343
+ if ("react-scripts" in deps || "@craco/craco" in deps)
344
+ return "cra";
318
345
  if ("fastify" in deps)
319
346
  return "fastify";
320
347
  if ("hono" in deps)
@@ -331,6 +358,8 @@ function nodePort(framework) {
331
358
  return 4321;
332
359
  case "vite":
333
360
  return 4173;
361
+ case "cra":
362
+ return 80;
334
363
  default:
335
364
  return 3000;
336
365
  }
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
5
  import { pingApi } from "./client.js";
6
+ import { MCP_VERSION } from "./version.js";
6
7
  import { DeployInputShape, RollbackInputShape, deployHandler, rollbackHandler, } from "./tools/deploy.js";
7
8
  import { PlansInputShape, plansHandler } from "./tools/plans.js";
8
9
  import { StatusInputShape, statusHandler } from "./tools/status.js";
@@ -27,7 +28,7 @@ const PlansSchema = z.object(PlansInputShape);
27
28
  async function main() {
28
29
  const server = new McpServer({
29
30
  name: "offlocal",
30
- version: "0.1.0",
31
+ version: MCP_VERSION,
31
32
  });
32
33
  server.tool("offlocal_ping", "Verify the Offlocal connection. Returns the signed-in email if connected, or an error message if not. This tool does NOT deploy anything, upload anything, or provision any resources - it is the right tool to call when the user asks to 'check my Offlocal connection' or similar.", PingInputShape, async (args) => pingHandler(PingSchema.parse(args)));
33
34
  server.tool("offlocal_plan", "START HERE when the user asks to deploy/ship/set up a project. Detects the app + the resources it needs (e.g. a Postgres database), applies the user's saved platform preferences, and returns a complete proposed plan (which platform for each piece — Offlocal.ai by default, or Supabase/PlanetScale/DigitalOcean/Railway). Present the plan to the user verbatim and let them confirm with 'go' or switch any line. Then execute the steps the plan lists. Do NOT deploy or provision anything until the user confirms the plan.", PlanInputShape, async (args) => planHandler(PlanSchema.parse(args)));
@@ -50,7 +51,16 @@ async function main() {
50
51
  process.on("unhandledRejection", (reason) => {
51
52
  console.error("offlocal-mcp unhandled rejection (continuing):", reason);
52
53
  });
53
- pingApi().catch(() => { });
54
+ pingApi()
55
+ .then((r) => {
56
+ if (!r.ok && r.mustUpdate) {
57
+ console.error(`offlocal-mcp: ${r.reason}`);
58
+ }
59
+ else if (r.ok && r.updateAvailable) {
60
+ console.error(`offlocal-mcp: a newer version (v${r.updateAvailable}) is available — update with: npx -y @offlocal/mcp@latest`);
61
+ }
62
+ })
63
+ .catch(() => { });
54
64
  const transport = new StdioServerTransport();
55
65
  await server.connect(transport);
56
66
  }
@@ -75,6 +75,10 @@ export async function deployHandler(input) {
75
75
  (bundle.warnings.length
76
76
  ? bundle.warnings.map((w) => `${ICON.warn} ${w}`).join("\n") + "\n\n"
77
77
  : "") +
78
+ (detected.notes?.length
79
+ ? detected.notes.map((n) => `${ICON.warn} ${n}`).join("\n\n") +
80
+ "\n\n"
81
+ : "") +
78
82
  `———\n` +
79
83
  `${ICON.arrow} **Next (you, the agent):** tell the user it's building (usually 2–8 min) and you'll ` +
80
84
  `report back when it's live. Then:\n` +
@@ -4,6 +4,19 @@ export const PingInputShape = {};
4
4
  export async function pingHandler(_input) {
5
5
  const result = await pingApi();
6
6
  if (!result.ok) {
7
+ if (result.mustUpdate) {
8
+ return {
9
+ content: [
10
+ {
11
+ type: "text",
12
+ text: `⚠️ Your Offlocal MCP is out of date and no longer supported.\n\n` +
13
+ `${result.reason}\n\n` +
14
+ "Update it with: npx -y @offlocal/mcp@latest (then restart the agent).",
15
+ },
16
+ ],
17
+ structuredContent: { connected: false, mustUpdate: true, reason: result.reason },
18
+ };
19
+ }
7
20
  return {
8
21
  content: [
9
22
  {
@@ -20,14 +33,21 @@ export async function pingHandler(_input) {
20
33
  };
21
34
  }
22
35
  const email = result.user?.email ?? "(unknown)";
36
+ const updateNote = result.updateAvailable
37
+ ? `\n\nℹ️ A newer Offlocal MCP (v${result.updateAvailable}) is available — update with: npx -y @offlocal/mcp@latest`
38
+ : "";
23
39
  return {
24
40
  content: [
25
41
  {
26
42
  type: "text",
27
- text: `Connected to Offlocal as ${email}.`,
43
+ text: `Connected to Offlocal as ${email}.${updateNote}`,
28
44
  },
29
45
  ],
30
- structuredContent: { connected: true, email },
46
+ structuredContent: {
47
+ connected: true,
48
+ email,
49
+ updateAvailable: result.updateAvailable ?? null,
50
+ },
31
51
  };
32
52
  }
33
53
  void z;
@@ -222,7 +222,7 @@ export async function createManagedDatabaseHandler(input) {
222
222
  const res = await createManagedDatabase(input.projectId);
223
223
  if (res.error) {
224
224
  const planHint = res.error === "plan_not_allowed" || res.error === "limit_reached"
225
- ? " A Hobby plan or higher is required, or the database limit was reached plans can be changed at /billing."
225
+ ? ` Two options: add a managed Postgres by upgrading at /billing, or connect your own database with \`offlocal_connect_provider\` (capability "postgres").`
226
226
  : "";
227
227
  return text(`${ICON.fail} Couldn't create the managed database: ${res.message ?? res.error}.${planHint}`, res);
228
228
  }
@@ -60,6 +60,10 @@ export async function statusHandler(input) {
60
60
  lines.push(dep.errorMessage.slice(0, 2000));
61
61
  lines.push("```");
62
62
  }
63
+ if (dep.errorType === "build_failed" || dep.errorType === "build_error") {
64
+ lines.push("");
65
+ lines.push(`Call \`offlocal_logs({ deploymentId: "${dep.id}" })\` for the full build output, fix the cause, then re-run \`offlocal_deploy\`.`);
66
+ }
63
67
  }
64
68
  else {
65
69
  lines.push(`${ICON.busy} **${phase}**`);
@@ -0,0 +1,9 @@
1
+ import { createRequire } from "node:module";
2
+ let version = "0.0.0";
3
+ try {
4
+ const req = createRequire(import.meta.url);
5
+ version = req("../package.json").version ?? version;
6
+ }
7
+ catch {
8
+ }
9
+ export const MCP_VERSION = version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@offlocal/mcp",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
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",