@offlocal/mcp 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 the author
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # @offlocal/mcp
2
+
3
+ The MCP (Model Context Protocol) server for [Offlocal](https://offlocal.ai) —
4
+ lets your AI coding agent (Claude Code, Cursor, Codex, Windsurf, etc.) deploy
5
+ apps directly from a coding session.
6
+
7
+ ## Install
8
+
9
+ Sign up at [offlocal.ai](https://offlocal.ai) to get an agent key, then add
10
+ this MCP server to your agent's config.
11
+
12
+ ### Claude Code
13
+
14
+ ```bash
15
+ claude mcp add offlocal \
16
+ --env OFFLOCAL_AGENT_KEY=off_live_xxx \
17
+ -- npx -y @offlocal/mcp
18
+ ```
19
+
20
+ ### Cursor (`~/.cursor/mcp.json`)
21
+
22
+ ```json
23
+ {
24
+ "mcpServers": {
25
+ "offlocal": {
26
+ "command": "npx",
27
+ "args": ["-y", "@offlocal/mcp"],
28
+ "env": { "OFFLOCAL_AGENT_KEY": "off_live_xxx" }
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ ### Codex (`~/.codex/config.toml`)
35
+
36
+ ```toml
37
+ [mcp_servers.offlocal]
38
+ command = "npx"
39
+ args = ["-y", "@offlocal/mcp"]
40
+ env = { OFFLOCAL_AGENT_KEY = "off_live_xxx" }
41
+ ```
42
+
43
+ ### Windsurf (`~/.codeium/windsurf/mcp_config.json`)
44
+
45
+ Same shape as Cursor.
46
+
47
+ ### Easier: let your agent install itself
48
+
49
+ Paste this into a fresh agent chat:
50
+
51
+ > Install Offlocal as an MCP server for me. Use `npx -y @offlocal/mcp` with env
52
+ > `OFFLOCAL_AGENT_KEY = off_live_xxx`. Use whatever config file or built-in
53
+ > command your agent uses. Tell me to restart when done.
54
+
55
+ ## Tools exposed
56
+
57
+ | Tool | What it does |
58
+ | --- | --- |
59
+ | `offlocal_ping` | Verify the agent is connected. Returns your email if so. |
60
+ | `offlocal_deploy` | Detect runtime, zip the source, upload, kick off the build + deploy pipeline. Returns a `deploymentId`. |
61
+ | `offlocal_status` | Poll a deployment. Returns the current phase, ETA, and the live URL on success. |
62
+ | `offlocal_logs` | Read the most recent container logs from CloudWatch. |
63
+
64
+ ## Env
65
+
66
+ | Variable | Required | Notes |
67
+ | --- | --- | --- |
68
+ | `OFFLOCAL_AGENT_KEY` | yes | From the Offlocal dashboard at offlocal.ai |
69
+ | `OFFLOCAL_API_URL` | no | Defaults to `https://api.offlocal.ai`. |
70
+
71
+ ## License
72
+
73
+ MIT
package/dist/client.js ADDED
@@ -0,0 +1,84 @@
1
+ const API_URL = process.env.OFFLOCAL_API_URL?.replace(/\/+$/, "") ?? "http://localhost:4000";
2
+ const AGENT_KEY = process.env.OFFLOCAL_AGENT_KEY ?? "";
3
+ function authHeaders() {
4
+ return AGENT_KEY ? { Authorization: `Bearer ${AGENT_KEY}` } : {};
5
+ }
6
+ export async function pingApi() {
7
+ if (!AGENT_KEY) {
8
+ return { ok: false, reason: "OFFLOCAL_AGENT_KEY env var is empty" };
9
+ }
10
+ const url = `${API_URL}/api/agent/ping`;
11
+ try {
12
+ const res = await fetch(url, {
13
+ method: "POST",
14
+ headers: { "content-type": "application/json", ...authHeaders() },
15
+ body: "{}",
16
+ });
17
+ if (!res.ok) {
18
+ const body = await res.text().catch(() => "");
19
+ return {
20
+ ok: false,
21
+ reason: `${res.status} ${res.statusText} from ${url}${body ? ` — ${body.slice(0, 200)}` : ""}`,
22
+ };
23
+ }
24
+ const data = (await res.json());
25
+ return { ok: true, user: data.user };
26
+ }
27
+ catch (err) {
28
+ const message = err instanceof Error ? err.message : String(err);
29
+ return {
30
+ ok: false,
31
+ reason: `fetch ${url} failed — ${message}`,
32
+ };
33
+ }
34
+ }
35
+ export async function createDeployment(input) {
36
+ const res = await fetch(`${API_URL}/api/deployments`, {
37
+ method: "POST",
38
+ headers: { "content-type": "application/json", ...authHeaders() },
39
+ body: JSON.stringify(input),
40
+ });
41
+ if (!res.ok)
42
+ throw new Error(await failureText(res, "create_deployment"));
43
+ return (await res.json());
44
+ }
45
+ export async function uploadBundle(url, bundle) {
46
+ const res = await fetch(url, {
47
+ method: "PUT",
48
+ headers: { "content-type": "application/zip" },
49
+ body: bundle,
50
+ });
51
+ if (!res.ok)
52
+ throw new Error(await failureText(res, "upload_bundle"));
53
+ }
54
+ export async function markUploadComplete(deploymentId) {
55
+ const res = await fetch(`${API_URL}/api/deployments/${encodeURIComponent(deploymentId)}/upload-complete`, { method: "POST", headers: { ...authHeaders() } });
56
+ if (!res.ok)
57
+ throw new Error(await failureText(res, "upload_complete"));
58
+ const json = (await res.json());
59
+ return json.deployment;
60
+ }
61
+ export async function getDeployment(deploymentId) {
62
+ const res = await fetch(`${API_URL}/api/deployments/${encodeURIComponent(deploymentId)}`, { headers: { ...authHeaders() } });
63
+ if (!res.ok)
64
+ throw new Error(await failureText(res, "get_deployment"));
65
+ const json = (await res.json());
66
+ return json.deployment;
67
+ }
68
+ export async function getDeploymentLogs(deploymentId, options) {
69
+ const params = new URLSearchParams();
70
+ if (options?.limit)
71
+ params.set("limit", String(options.limit));
72
+ const qs = params.toString();
73
+ const url = `${API_URL}/api/deployments/${encodeURIComponent(deploymentId)}/logs${qs ? `?${qs}` : ""}`;
74
+ const res = await fetch(url, { headers: { ...authHeaders() } });
75
+ if (!res.ok)
76
+ throw new Error(await failureText(res, "get_logs"));
77
+ const json = (await res.json());
78
+ return json.logs;
79
+ }
80
+ async function failureText(res, kind) {
81
+ const body = await res.text().catch(() => "");
82
+ return `${kind} failed: ${res.status} ${res.statusText}${body ? ` — ${body.slice(0, 400)}` : ""}`;
83
+ }
84
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/detect.js ADDED
@@ -0,0 +1,108 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import { basename, join } from "node:path";
3
+ const DEFAULTS = {
4
+ runtime: "node-20",
5
+ framework: "node",
6
+ buildCommand: "echo no-build",
7
+ startCommand: "npm start",
8
+ port: 3000,
9
+ };
10
+ export async function detect(sourceDir) {
11
+ const pkgPath = join(sourceDir, "package.json");
12
+ let pkg = null;
13
+ if (await fileExists(pkgPath)) {
14
+ try {
15
+ const raw = await readFile(pkgPath, "utf8");
16
+ pkg = JSON.parse(raw);
17
+ }
18
+ catch {
19
+ pkg = null;
20
+ }
21
+ }
22
+ const appName = pkg?.name ?? slugifyDirName(sourceDir) ?? "app";
23
+ const deps = {
24
+ ...(pkg?.dependencies ?? {}),
25
+ ...(pkg?.devDependencies ?? {}),
26
+ };
27
+ const scripts = pkg?.scripts ?? {};
28
+ const framework = pickFramework(deps);
29
+ const port = portFor(framework);
30
+ const buildCommand = scripts.build ? "npm run build" : DEFAULTS.buildCommand;
31
+ const startCommand = scripts.start
32
+ ? "npm start"
33
+ : pickFallbackStart(framework);
34
+ return {
35
+ appName,
36
+ runtime: DEFAULTS.runtime,
37
+ framework,
38
+ buildCommand,
39
+ startCommand,
40
+ port,
41
+ };
42
+ }
43
+ function pickFramework(deps) {
44
+ if ("next" in deps)
45
+ return "nextjs";
46
+ if ("nuxt" in deps)
47
+ return "nuxt";
48
+ if ("@remix-run/serve" in deps || "@remix-run/node" in deps)
49
+ return "remix";
50
+ if ("astro" in deps)
51
+ return "astro";
52
+ if ("fastify" in deps)
53
+ return "fastify";
54
+ if ("hono" in deps)
55
+ return "hono";
56
+ if ("express" in deps)
57
+ return "express";
58
+ if ("koa" in deps)
59
+ return "koa";
60
+ if ("vite" in deps)
61
+ return "vite";
62
+ return "node";
63
+ }
64
+ function portFor(framework) {
65
+ switch (framework) {
66
+ case "nextjs":
67
+ return 3000;
68
+ case "nuxt":
69
+ return 3000;
70
+ case "remix":
71
+ return 3000;
72
+ case "astro":
73
+ return 4321;
74
+ case "vite":
75
+ return 4173;
76
+ case "fastify":
77
+ return 3000;
78
+ case "hono":
79
+ return 3000;
80
+ case "express":
81
+ return 3000;
82
+ case "koa":
83
+ return 3000;
84
+ default:
85
+ return 3000;
86
+ }
87
+ }
88
+ function pickFallbackStart(framework) {
89
+ if (framework === "nextjs")
90
+ return "npx next start";
91
+ if (framework === "nuxt")
92
+ return "node .output/server/index.mjs";
93
+ return "node index.js";
94
+ }
95
+ function slugifyDirName(dir) {
96
+ const name = basename(dir).toLowerCase().replace(/[^a-z0-9-]+/g, "-");
97
+ return name || null;
98
+ }
99
+ async function fileExists(path) {
100
+ try {
101
+ await stat(path);
102
+ return true;
103
+ }
104
+ catch {
105
+ return false;
106
+ }
107
+ }
108
+ //# sourceMappingURL=detect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect.js","sourceRoot":"","sources":["../src/detect.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { pingApi } from "./client.js";
6
+ import { DeployInputShape, deployHandler } from "./tools/deploy.js";
7
+ import { StatusInputShape, statusHandler } from "./tools/status.js";
8
+ import { LogsInputShape, logsHandler } from "./tools/logs.js";
9
+ import { PingInputShape, pingHandler } from "./tools/ping.js";
10
+ const DeploySchema = z.object(DeployInputShape);
11
+ const StatusSchema = z.object(StatusInputShape);
12
+ const LogsSchema = z.object(LogsInputShape);
13
+ const PingSchema = z.object(PingInputShape);
14
+ async function main() {
15
+ const server = new McpServer({
16
+ name: "offlocal",
17
+ version: "0.1.0",
18
+ });
19
+ 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 call AWS — 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)));
20
+ server.tool("offlocal_deploy", "Deploy an app from a local directory to Offlocal. Returns a deploymentId you can poll with offlocal_status. Detects runtime/framework/port automatically; you can override appName and port. Only call this when the user explicitly asks to deploy.", DeployInputShape, async (args) => deployHandler(DeploySchema.parse(args)));
21
+ server.tool("offlocal_status", "Get the current status of a deployment. Poll this every few seconds after offlocal_deploy until status is 'live' or 'failed'.", StatusInputShape, async (args) => statusHandler(StatusSchema.parse(args)));
22
+ server.tool("offlocal_logs", "Read recent container logs for a deployment. Useful for debugging a failed deploy or watching live output.", LogsInputShape, async (args) => logsHandler(LogsSchema.parse(args)));
23
+ // Fire-and-forget ping so the dashboard knows the agent is connected.
24
+ // Don't block startup if it fails — agent will still work.
25
+ pingApi().catch(() => { });
26
+ const transport = new StdioServerTransport();
27
+ await server.connect(transport);
28
+ }
29
+ main().catch((err) => {
30
+ console.error("offlocal-mcp fatal:", err);
31
+ process.exit(1);
32
+ });
33
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9D,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAChD,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAChD,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;AAC5C,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;AAE5C,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,OAAO;KACjB,CAAC,CAAC;IAEH,MAAM,CAAC,IAAI,CACT,eAAe,EACf,wQAAwQ,EACxQ,cAAc,EACd,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CACpD,CAAC;IAEF,MAAM,CAAC,IAAI,CACT,iBAAiB,EACjB,sPAAsP,EACtP,gBAAgB,EAChB,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CACxD,CAAC;IAEF,MAAM,CAAC,IAAI,CACT,iBAAiB,EACjB,+HAA+H,EAC/H,gBAAgB,EAChB,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CACxD,CAAC;IAEF,MAAM,CAAC,IAAI,CACT,eAAe,EACf,4GAA4G,EAC5G,cAAc,EACd,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CACpD,CAAC;IAEF,sEAAsE;IACtE,2DAA2D;IAC3D,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAE1B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,qBAAqB,EAAE,GAAG,CAAC,CAAC;IAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,79 @@
1
+ import { z } from "zod";
2
+ import { createDeployment, markUploadComplete, uploadBundle, } from "../client.js";
3
+ import { detect } from "../detect.js";
4
+ import { bundleDirectory } from "../zip.js";
5
+ export const DeployInputShape = {
6
+ sourceDir: z
7
+ .string()
8
+ .min(1)
9
+ .describe("Absolute path to the project directory to deploy. Use the project root (where package.json lives)."),
10
+ appName: z
11
+ .string()
12
+ .min(1)
13
+ .max(63)
14
+ .optional()
15
+ .describe("Optional. Defaults to package.json `name` or the folder name. Becomes the subdomain: {appName}.offlocal.ai"),
16
+ port: z
17
+ .number()
18
+ .int()
19
+ .min(1)
20
+ .max(65535)
21
+ .optional()
22
+ .describe("Optional. The port the container listens on. Auto-detected from framework."),
23
+ };
24
+ export async function deployHandler(input) {
25
+ const detected = await detect(input.sourceDir);
26
+ const appName = input.appName ?? detected.appName;
27
+ const port = input.port ?? detected.port;
28
+ const bundle = await bundleDirectory(input.sourceDir);
29
+ const created = await createDeployment({
30
+ appName,
31
+ runtime: detected.runtime,
32
+ framework: detected.framework,
33
+ buildCommand: detected.buildCommand,
34
+ startCommand: detected.startCommand,
35
+ port,
36
+ });
37
+ await uploadBundle(created.upload.url, bundle.buffer);
38
+ await markUploadComplete(created.deploymentId);
39
+ const summary = {
40
+ deploymentId: created.deploymentId,
41
+ appName,
42
+ framework: detected.framework,
43
+ port,
44
+ bundle: {
45
+ fileCount: bundle.fileCount,
46
+ bytes: bundle.byteSize,
47
+ },
48
+ status: "uploaded",
49
+ expectedUrl: `https://${appName}.offlocal.ai`,
50
+ pollAfterSeconds: 45,
51
+ expectedTotalSeconds: 180,
52
+ };
53
+ return {
54
+ content: [
55
+ {
56
+ type: "text",
57
+ text: `Deployment created and source uploaded.\n\n` +
58
+ ` deploymentId: ${summary.deploymentId}\n` +
59
+ ` appName: ${summary.appName}\n` +
60
+ ` framework: ${summary.framework}\n` +
61
+ ` port: ${summary.port}\n` +
62
+ ` bundle: ${summary.bundle.fileCount} files, ${summary.bundle.bytes} bytes\n` +
63
+ ` expectedUrl: ${summary.expectedUrl}\n\n` +
64
+ `WAIT — this is a normal, slow operation.\n` +
65
+ `Typical end-to-end time: 2-4 minutes (CodeBuild cold start + ECS rollout).\n` +
66
+ `Do NOT poll offlocal_status faster than once every 30 seconds. ` +
67
+ `Repeated polling does not speed anything up; the only thing that helps is time.\n\n` +
68
+ `Recommended sequence:\n` +
69
+ ` 1. Sleep 45 seconds.\n` +
70
+ ` 2. Call offlocal_status({ deploymentId: "${summary.deploymentId}" }).\n` +
71
+ ` 3. The response will include "pollAfterSeconds" — sleep that long, then call status again.\n` +
72
+ ` 4. Continue until status is 'live' or 'failed'.\n\n` +
73
+ `Tell the user the deploy is in progress and you'll report back when it's live.`,
74
+ },
75
+ ],
76
+ structuredContent: summary,
77
+ };
78
+ }
79
+ //# sourceMappingURL=deploy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deploy.js","sourceRoot":"","sources":["../../src/tools/deploy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,GACb,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,SAAS,EAAE,CAAC;SACT,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,QAAQ,CACP,oGAAoG,CACrG;IACH,OAAO,EAAE,CAAC;SACP,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,EAAE,CAAC;SACP,QAAQ,EAAE;SACV,QAAQ,CACP,4GAA4G,CAC7G;IACH,IAAI,EAAE,CAAC;SACJ,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,KAAK,CAAC;SACV,QAAQ,EAAE;SACV,QAAQ,CACP,4EAA4E,CAC7E;CACK,CAAC;AAQX,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAkB;IACpD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IAC/C,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,QAAQ,CAAC,OAAO,CAAC;IAClD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,IAAI,QAAQ,CAAC,IAAI,CAAC;IAEzC,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IAEtD,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC;QACrC,OAAO;QACP,OAAO,EAAE,QAAQ,CAAC,OAAO;QACzB,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,YAAY,EAAE,QAAQ,CAAC,YAAY;QACnC,YAAY,EAAE,QAAQ,CAAC,YAAY;QACnC,IAAI;KACL,CAAC,CAAC;IAEH,MAAM,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IACtD,MAAM,kBAAkB,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IAE/C,MAAM,OAAO,GAAG;QACd,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,OAAO;QACP,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,IAAI;QACJ,MAAM,EAAE;YACN,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,KAAK,EAAE,MAAM,CAAC,QAAQ;SACvB;QACD,MAAM,EAAE,UAAU;QAClB,WAAW,EAAE,WAAW,OAAO,cAAc;QAC7C,gBAAgB,EAAE,EAAE;QACpB,oBAAoB,EAAE,GAAG;KAC1B,CAAC;IAEF,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI,EACF,6CAA6C;oBAC7C,mBAAmB,OAAO,CAAC,YAAY,IAAI;oBAC3C,mBAAmB,OAAO,CAAC,OAAO,IAAI;oBACtC,mBAAmB,OAAO,CAAC,SAAS,IAAI;oBACxC,mBAAmB,OAAO,CAAC,IAAI,IAAI;oBACnC,mBAAmB,OAAO,CAAC,MAAM,CAAC,SAAS,WAAW,OAAO,CAAC,MAAM,CAAC,KAAK,UAAU;oBACpF,mBAAmB,OAAO,CAAC,WAAW,MAAM;oBAC5C,4CAA4C;oBAC5C,8EAA8E;oBAC9E,iEAAiE;oBACjE,qFAAqF;oBACrF,yBAAyB;oBACzB,0BAA0B;oBAC1B,8CAA8C,OAAO,CAAC,YAAY,SAAS;oBAC3E,gGAAgG;oBAChG,uDAAuD;oBACvD,gFAAgF;aACnF;SACF;QACD,iBAAiB,EAAE,OAAO;KAC3B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,38 @@
1
+ import { z } from "zod";
2
+ import { getDeploymentLogs } from "../client.js";
3
+ export const LogsInputShape = {
4
+ deploymentId: z
5
+ .string()
6
+ .min(1)
7
+ .describe("The deploymentId whose container logs you want to read."),
8
+ limit: z
9
+ .number()
10
+ .int()
11
+ .min(1)
12
+ .max(500)
13
+ .optional()
14
+ .describe("Maximum number of log lines to return (default 100, max 500)."),
15
+ };
16
+ export async function logsHandler(input) {
17
+ const logs = await getDeploymentLogs(input.deploymentId, {
18
+ limit: input.limit,
19
+ });
20
+ if (logs.length === 0) {
21
+ return {
22
+ content: [
23
+ {
24
+ type: "text",
25
+ text: "No logs available yet. The container may not be running, or no log stream exists for this project. " +
26
+ "Wait until offlocal_status reports 'live', then try again.",
27
+ },
28
+ ],
29
+ structuredContent: { logs: [] },
30
+ };
31
+ }
32
+ const text = logs.map((l) => `${l.at} ${l.message}`).join("\n");
33
+ return {
34
+ content: [{ type: "text", text }],
35
+ structuredContent: { logs },
36
+ };
37
+ }
38
+ //# sourceMappingURL=logs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logs.js","sourceRoot":"","sources":["../../src/tools/logs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEjD,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,YAAY,EAAE,CAAC;SACZ,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,QAAQ,CAAC,yDAAyD,CAAC;IACtE,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,GAAG,CAAC;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,+DAA+D,CAAC;CACpE,CAAC;AAIX,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,KAAgB;IAChD,MAAM,IAAI,GAAG,MAAM,iBAAiB,CAAC,KAAK,CAAC,YAAY,EAAE;QACvD,KAAK,EAAE,KAAK,CAAC,KAAK;KACnB,CAAC,CAAC;IAEH,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAe;oBACrB,IAAI,EACF,qGAAqG;wBACrG,4DAA4D;iBAC/D;aACF;YACD,iBAAiB,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;SAChC,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEjE,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,CAAC;QAC1C,iBAAiB,EAAE,EAAE,IAAI,EAAE;KAC5B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,35 @@
1
+ import { z } from "zod";
2
+ import { pingApi } from "../client.js";
3
+ export const PingInputShape = {};
4
+ export async function pingHandler(_input) {
5
+ const result = await pingApi();
6
+ if (!result.ok) {
7
+ return {
8
+ content: [
9
+ {
10
+ type: "text",
11
+ text: "Not connected to Offlocal.\n\n" +
12
+ `Reason: ${result.reason}\n\n` +
13
+ "Common fixes:\n" +
14
+ " - If you're calling from WSL, make sure OFFLOCAL_API_URL points at the Windows host (http://host.docker.internal:4000) — not localhost.\n" +
15
+ " - Make sure `pnpm dev:api` is running on the Windows side.\n" +
16
+ " - Double-check OFFLOCAL_AGENT_KEY matches a key created in the dashboard.",
17
+ },
18
+ ],
19
+ structuredContent: { connected: false, reason: result.reason },
20
+ };
21
+ }
22
+ const email = result.user?.email ?? "(unknown)";
23
+ return {
24
+ content: [
25
+ {
26
+ type: "text",
27
+ text: `Connected to Offlocal as ${email}.`,
28
+ },
29
+ ],
30
+ structuredContent: { connected: true, email },
31
+ };
32
+ }
33
+ // Silence unused-import warnings if z isn't used after init.
34
+ void z;
35
+ //# sourceMappingURL=ping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ping.js","sourceRoot":"","sources":["../../src/tools/ping.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,MAAM,CAAC,MAAM,cAAc,GAAG,EAAW,CAAC;AAI1C,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAiB;IACjD,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,CAAC;IAE/B,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAe;oBACrB,IAAI,EACF,gCAAgC;wBAChC,WAAW,MAAM,CAAC,MAAM,MAAM;wBAC9B,iBAAiB;wBACjB,6IAA6I;wBAC7I,gEAAgE;wBAChE,6EAA6E;iBAChF;aACF;YACD,iBAAiB,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;SAC/D,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,KAAK,IAAI,WAAW,CAAC;IAChD,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI,EAAE,4BAA4B,KAAK,GAAG;aAC3C;SACF;QACD,iBAAiB,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE;KAC9C,CAAC;AACJ,CAAC;AAED,6DAA6D;AAC7D,KAAK,CAAC,CAAC"}
@@ -0,0 +1,116 @@
1
+ import { z } from "zod";
2
+ import { getDeployment } from "../client.js";
3
+ export const StatusInputShape = {
4
+ deploymentId: z
5
+ .string()
6
+ .min(1)
7
+ .describe("The deploymentId returned by offlocal_deploy."),
8
+ };
9
+ export async function statusHandler(input) {
10
+ const dep = await getDeployment(input.deploymentId);
11
+ const phase = phaseFor(dep.status);
12
+ const isTerminal = dep.status === "live" || dep.status === "failed";
13
+ const pollAfterSeconds = isTerminal ? 0 : pollIntervalFor(dep.status);
14
+ const etaSeconds = isTerminal ? 0 : etaFor(dep.status);
15
+ const summary = {
16
+ deploymentId: dep.id,
17
+ status: dep.status,
18
+ phase,
19
+ isTerminal,
20
+ pollAfterSeconds,
21
+ etaSeconds,
22
+ liveUrl: dep.liveUrl,
23
+ errorType: dep.errorType,
24
+ errorMessage: dep.errorMessage,
25
+ updatedAt: dep.updatedAt,
26
+ };
27
+ const lines = [`Status: ${dep.status} (${phase})`];
28
+ if (dep.status === "live" && dep.liveUrl) {
29
+ lines.push("");
30
+ lines.push(`Live at ${dep.liveUrl}`);
31
+ }
32
+ else if (dep.status === "failed") {
33
+ lines.push("");
34
+ lines.push(`Failed: ${dep.errorType ?? "unknown"}`);
35
+ if (dep.errorMessage) {
36
+ lines.push("");
37
+ lines.push(dep.errorMessage.slice(0, 2000));
38
+ }
39
+ }
40
+ else {
41
+ lines.push("");
42
+ lines.push(`Still in progress. Typical end-to-end deploy: 2-4 minutes from upload.`);
43
+ lines.push(`Do NOT call this tool again for at least ${pollAfterSeconds} seconds — there's no faster way to make the deploy go.`);
44
+ lines.push(`If you must wait, just sleep ${pollAfterSeconds} seconds and call this tool exactly once. Repeated polling will not speed anything up.`);
45
+ }
46
+ return {
47
+ content: [{ type: "text", text: lines.join("\n") }],
48
+ structuredContent: summary,
49
+ };
50
+ }
51
+ function phaseFor(status) {
52
+ switch (status) {
53
+ case "created":
54
+ case "upload_pending":
55
+ return "waiting for source upload";
56
+ case "uploaded":
57
+ case "validating":
58
+ return "validating source";
59
+ case "queued":
60
+ return "queued for build";
61
+ case "building":
62
+ case "image_built":
63
+ return "building image (CodeBuild)";
64
+ case "deploying":
65
+ return "starting container (ECS rollout)";
66
+ case "live":
67
+ return "live";
68
+ case "failed":
69
+ return "failed";
70
+ case "deleted":
71
+ return "deleted";
72
+ default:
73
+ return "unknown";
74
+ }
75
+ }
76
+ /**
77
+ * How long the agent should wait before calling this tool again.
78
+ * Tuned to typical AWS step latencies so we don't hammer the API.
79
+ */
80
+ function pollIntervalFor(status) {
81
+ switch (status) {
82
+ case "upload_pending":
83
+ return 5;
84
+ case "uploaded":
85
+ case "validating":
86
+ return 10;
87
+ case "queued":
88
+ case "building":
89
+ case "image_built":
90
+ return 30;
91
+ case "deploying":
92
+ return 30;
93
+ default:
94
+ return 15;
95
+ }
96
+ }
97
+ /** Rough remaining-time estimate in seconds. */
98
+ function etaFor(status) {
99
+ switch (status) {
100
+ case "upload_pending":
101
+ return 30;
102
+ case "uploaded":
103
+ case "validating":
104
+ return 15;
105
+ case "queued":
106
+ return 90;
107
+ case "building":
108
+ case "image_built":
109
+ return 75;
110
+ case "deploying":
111
+ return 90;
112
+ default:
113
+ return 0;
114
+ }
115
+ }
116
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/tools/status.ts"],"names":[],"mappings":"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"}
package/dist/zip.js ADDED
@@ -0,0 +1,76 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import { join, relative, sep } from "node:path";
3
+ import AdmZip from "adm-zip";
4
+ const IGNORED_DIRS = new Set([
5
+ "node_modules",
6
+ ".git",
7
+ ".next",
8
+ ".nuxt",
9
+ ".output",
10
+ ".astro",
11
+ ".svelte-kit",
12
+ "dist",
13
+ "build",
14
+ "out",
15
+ "coverage",
16
+ ".turbo",
17
+ ".cache",
18
+ ".vercel",
19
+ ".netlify",
20
+ ".idea",
21
+ ".vscode",
22
+ ]);
23
+ const IGNORED_FILE_RE = /^(\.env(\..+)?|.*\.log|\.DS_Store|Thumbs\.db|.*\.tsbuildinfo)$/;
24
+ const MAX_FILE_BYTES = 50 * 1024 * 1024;
25
+ const MAX_BUNDLE_BYTES = 250 * 1024 * 1024;
26
+ export async function bundleDirectory(sourceDir) {
27
+ const zip = new AdmZip();
28
+ let fileCount = 0;
29
+ let totalBytes = 0;
30
+ await walk(sourceDir, async (absolute) => {
31
+ const rel = relative(sourceDir, absolute);
32
+ if (!rel)
33
+ return;
34
+ const st = await stat(absolute);
35
+ if (!st.isFile())
36
+ return;
37
+ if (st.size > MAX_FILE_BYTES) {
38
+ throw new Error(`file too large to bundle: ${rel} (${st.size} bytes, max ${MAX_FILE_BYTES})`);
39
+ }
40
+ const content = await readFile(absolute);
41
+ const zipPath = rel.split(sep).join("/");
42
+ zip.addFile(zipPath, content);
43
+ fileCount++;
44
+ totalBytes += st.size;
45
+ if (totalBytes > MAX_BUNDLE_BYTES) {
46
+ throw new Error(`bundle exceeds ${MAX_BUNDLE_BYTES} bytes. Trim node_modules-like outputs or add a .offlocalignore.`);
47
+ }
48
+ });
49
+ if (fileCount === 0) {
50
+ throw new Error(`no files to bundle under ${sourceDir}`);
51
+ }
52
+ return { buffer: zip.toBuffer(), fileCount, byteSize: totalBytes };
53
+ }
54
+ async function walk(root, onFile) {
55
+ const stack = [root];
56
+ while (stack.length) {
57
+ const current = stack.pop();
58
+ if (!current)
59
+ break;
60
+ const entries = await readdir(current, { withFileTypes: true });
61
+ for (const entry of entries) {
62
+ const absolute = join(current, entry.name);
63
+ if (entry.isDirectory()) {
64
+ if (IGNORED_DIRS.has(entry.name))
65
+ continue;
66
+ stack.push(absolute);
67
+ }
68
+ else if (entry.isFile()) {
69
+ if (IGNORED_FILE_RE.test(entry.name))
70
+ continue;
71
+ await onFile(absolute);
72
+ }
73
+ }
74
+ }
75
+ }
76
+ //# sourceMappingURL=zip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zip.js","sourceRoot":"","sources":["../src/zip.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@offlocal/mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Offlocal — lets your AI coding agent deploy apps via offlocal_deploy, offlocal_status, offlocal_logs, offlocal_ping.",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "offlocal",
9
+ "deploy",
10
+ "claude-code",
11
+ "cursor",
12
+ "codex",
13
+ "windsurf",
14
+ "ai-agent"
15
+ ],
16
+ "homepage": "https://offlocal.ai",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/offlocal/offlocal.git",
20
+ "directory": "apps/mcp"
21
+ },
22
+ "license": "MIT",
23
+ "author": "the author",
24
+ "type": "module",
25
+ "bin": {
26
+ "offlocal-mcp": "dist/index.js"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "engines": {
34
+ "node": ">=20"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "1.17.0",
41
+ "adm-zip": "0.5.16",
42
+ "zod": "3.25.76"
43
+ },
44
+ "devDependencies": {
45
+ "@types/adm-zip": "0.5.5",
46
+ "@types/node": "20.14.10",
47
+ "tsx": "4.19.1",
48
+ "typescript": "5.5.3"
49
+ },
50
+ "scripts": {
51
+ "dev": "tsx src/index.ts",
52
+ "build": "tsc -p tsconfig.json && node -e \"try{require('fs').chmodSync('dist/index.js', 0o755)}catch(e){}\"",
53
+ "start": "node dist/index.js",
54
+ "typecheck": "tsc --noEmit"
55
+ }
56
+ }