alora-mcp 0.5.0 → 0.7.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.
Files changed (3) hide show
  1. package/README.md +62 -0
  2. package/package.json +3 -3
  3. package/src/index.js +68 -6
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # alora-mcp
2
+
3
+ MCP server for [Alora](https://alora.build) — the cloud where your agent works.
4
+
5
+ Give your coding agent a place to **actually ship**: it deploys the apps it writes to real infrastructure, shares them by email, previews changes before production, runs 24/7 workers and cron jobs, and even buys domains (with your approval). You talk to the agent; the agent talks to Alora.
6
+
7
+ Works with Claude Code, Codex, Cursor, and any MCP client.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ # Claude Code
13
+ claude mcp add alora -- npx alora-mcp
14
+
15
+ # Codex
16
+ codex mcp add alora -- npx alora-mcp
17
+ ```
18
+
19
+ Or in any MCP client's JSON config:
20
+
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "alora": { "command": "npx", "args": ["alora-mcp"] }
25
+ }
26
+ }
27
+ ```
28
+
29
+ ## Connect (once, takes a minute)
30
+
31
+ 1. Create a free account at [alora.build/register](https://alora.build/register) — $0.50 of credit included, no card.
32
+ 2. Run `npx alora-cli connect` in your terminal — it prints a code.
33
+ 3. Paste the code at [alora.build/activate](https://alora.build/activate).
34
+
35
+ The token lands in `~/.alora/config.json`, and from then on your agent takes it from here. Full playbook for agents: [alora.build/llms.txt](https://alora.build/llms.txt).
36
+
37
+ ## What your agent can do (45 tools)
38
+
39
+ | | |
40
+ |---|---|
41
+ | **Ship** | `create_project` · `deploy` (Node, Python, static, or any command via Procfile/alora.json) · `list_versions` · `rollback` |
42
+ | **Preview before prod** | `preview` → temporary URL at `myapp--preview.alora.page` with throwaway data · `promote` · `preview_stop` |
43
+ | **Share** | apps are born private — `share` by email with roles, `set_visibility`, `revoke_access` |
44
+ | **Run things** | `set_always_on` / `set_worker_mode` for 24/7 processes · `cron_create` for scheduled hits · `exec` · `restart` · `logs` |
45
+ | **Configure** | `secrets_set` (encrypted, injected as env) · `domain_connect` · `egress_ip_allocate` (static egress IP) |
46
+ | **Use AI inside the app** | `ai_enable` — the deployed app runs `claude -p` with the owner's own Claude account |
47
+ | **Buy domains** | `domain_search` · `domain_buy` — always gated on the owner's approval by email |
48
+ | **Know what's happening** | `analytics_summary` · `analytics_users` · `wallet_balance` · `changelog` |
49
+
50
+ ## How it behaves
51
+
52
+ - **Apps are born private.** Visitors get a login page; the owner invites people by email. Your agent never writes auth code — identity arrives as trusted headers.
53
+ - **Spending has a hard ceiling.** Prepaid credits only: $0.01 per active hour, $0 while an app sleeps, $0.40/GB/month storage. An agent can't spend what you didn't load. Typical side project: under $1/month.
54
+ - **Irreversible actions ask a human.** Domain purchases require the owner to click an approval email.
55
+ - **Data persists.** Each app gets a data directory that survives deploys, restarts and sleep.
56
+
57
+ ## Links
58
+
59
+ - Website: [alora.build](https://alora.build) · [Pricing](https://alora.build/pricing) · [Blog](https://alora.build/blog)
60
+ - Agent docs: [alora.build/llms.txt](https://alora.build/llms.txt)
61
+ - CLI for humans: [`alora-cli`](https://www.npmjs.com/package/alora-cli) · API client: [`alora-sdk`](https://www.npmjs.com/package/alora-sdk)
62
+ - Feedback (agents welcome): the `send_feedback` tool, or hola@alora.build
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "alora-mcp",
3
- "version": "0.5.0",
4
- "description": "Servidor MCP de Alora — Claude Code, Codex, Cursor y cualquier cliente MCP",
3
+ "version": "0.7.0",
4
+ "description": "MCP server for Alora, the cloud where your agent works deploy, preview, share and run the apps your agent writes. Claude Code, Codex, Cursor and any MCP client.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "alora-mcp": "src/index.js"
8
8
  },
9
9
  "dependencies": {
10
- "alora-sdk": "^0.5.0",
10
+ "alora-sdk": "^0.7.0",
11
11
  "@modelcontextprotocol/sdk": "^1.12.0",
12
12
  "tar": "^7.4.3",
13
13
  "zod": "^3.24.0"
package/src/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
6
  import { z } from 'zod';
7
- import { readFileSync, rmSync } from 'node:fs';
7
+ import { readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
8
8
  import { homedir, tmpdir } from 'node:os';
9
9
  import path from 'node:path';
10
10
  import * as tar from 'tar';
@@ -18,7 +18,7 @@ function client() {
18
18
  return new AloraClient({ api: cfg.api || 'https://api.alora.build', token: cfg.token });
19
19
  }
20
20
 
21
- const server = new McpServer({ name: 'alora', version: '0.1.0' });
21
+ const server = new McpServer({ name: 'alora', version: '0.7.0' });
22
22
 
23
23
  const ok = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
24
24
  const asError = (e) => ({
@@ -43,6 +43,30 @@ server.registerTool('whoami', {
43
43
  inputSchema: {},
44
44
  }, wrap(() => client().whoami()));
45
45
 
46
+ // --- alta iniciada por el agente: el usuario solo toca UN email ---
47
+ server.registerTool('account_create_and_connect', {
48
+ description: 'SIN cuenta de Alora todavía: preguntale al usuario su email EN EL CHAT y llamá esta tool. Le llega UN email; con un click se crea la cuenta (si no existe), se verifica, se activan sus US$0.50 gratis y este agente queda autorizado. Después llamá account_connect_poll con el device_code hasta que apruebe (el link vence en 15 min). NUNCA inventes un email ni uses uno descartable — tiene que ser el inbox real del usuario. Si el email ya tiene cuenta, le llega un pedido de autorización al flujo normal (login + Authorize).',
49
+ inputSchema: {
50
+ email: z.string().describe('El email que el usuario te dio en el chat'),
51
+ agent_name: z.string().optional().describe('Cómo se llama este agente, ej: "Claude de Ana"'),
52
+ },
53
+ }, wrap(({ email, agent_name }) => client().signupStart(email, agent_name || 'agente MCP')));
54
+
55
+ server.registerTool('account_connect_poll', {
56
+ description: 'Después de account_create_and_connect: llamá esto cada ~5s con el device_code. Cuando el usuario toque el email devuelve approved y GUARDA el token automáticamente — desde ahí todas las demás tools funcionan. expired = el link venció sin click: pedile al usuario que revise el inbox y volvé a empezar.',
57
+ inputSchema: { device_code: z.string() },
58
+ }, wrap(async ({ device_code }) => {
59
+ const res = await client().devicePoll(device_code);
60
+ if (res.status === 'approved' && res.token) {
61
+ let cfg = {};
62
+ try { cfg = JSON.parse(readFileSync(CONFIG_FILE, 'utf8')); } catch {}
63
+ mkdirSync(path.dirname(CONFIG_FILE), { recursive: true });
64
+ writeFileSync(CONFIG_FILE, JSON.stringify({ ...cfg, api: cfg.api || 'https://api.alora.build', token: res.token }, null, 2));
65
+ return { status: 'approved', note: 'Token guardado — ya podés usar todas las tools (probá whoami).' };
66
+ }
67
+ return res;
68
+ }));
69
+
46
70
  server.registerTool('list_projects', {
47
71
  description: 'Lista los proyectos del workspace con URL, versión activa y estado.',
48
72
  inputSchema: {},
@@ -54,13 +78,19 @@ server.registerTool('create_project', {
54
78
  }, wrap(({ name }) => client().createProject(name)));
55
79
 
56
80
  server.registerTool('deploy', {
57
- description: 'Despliega un directorio local como nueva versión del proyecto. Detecta el runtime solo (Node, Python, estático). Devuelve la URL.',
81
+ description: 'Despliega un directorio local como nueva versión del proyecto. Detecta el runtime solo (Node, Python, estático). Arranque: "main"/server.js, `npm start`, un Procfile ("web: ...") o alora.json {"start": "cualquier comando"}. Frontends (Vite/Next/Astro…): pasá build (ej: "npm run build") y dir apuntando a la salida (dist/). Estáticos tienen fallback SPA a index.html. Devuelve la URL.',
58
82
  inputSchema: {
59
83
  project: z.string().describe('Nombre del proyecto'),
60
- dir: z.string().describe('Ruta absoluta del directorio a desplegar'),
84
+ dir: z.string().describe('Ruta absoluta del directorio a desplegar (para un frontend buildeado, la carpeta de salida: dist/, build/, out/)'),
61
85
  message: z.string().optional().describe('Mensaje corto describiendo el cambio'),
86
+ build: z.string().optional().describe('Comando a correr antes (en el directorio del proyecto), ej: "npm run build"'),
87
+ base_version: z.number().optional().describe('Versión de la que partiste: si otro agente desplegó mientras tanto, falla con version_conflict en vez de pisar su trabajo'),
62
88
  },
63
- }, wrap(async ({ project, dir, message }) => {
89
+ }, wrap(async ({ project, dir, message, build, base_version }) => {
90
+ if (build) {
91
+ const { execSync } = await import('node:child_process');
92
+ execSync(build, { cwd: dir, stdio: 'ignore' });
93
+ }
64
94
  const tmpFile = path.join(tmpdir(), `alora-mcp-${Date.now()}.tgz`);
65
95
  await tar.create(
66
96
  {
@@ -71,9 +101,41 @@ server.registerTool('deploy', {
71
101
  );
72
102
  const buffer = readFileSync(tmpFile);
73
103
  rmSync(tmpFile, { force: true });
74
- return client().deploy(project, buffer, { message });
104
+ return client().deploy(project, buffer, { message, base_version });
105
+ }));
106
+
107
+ server.registerTool('preview', {
108
+ description: 'Despliega un candidato a una URL TEMPORAL (<proyecto>--preview) SIN tocar la versión viva. El loop con el humano: subís el cambio acá, le pasás el link para que lo mire, y recién si le gusta lo promovés a producción con `promote`. Datos efímeros (no comparte el /data de prod). Expira en 24h. Mismo runtime/detección/build que deploy. Devolvé preview_url al humano.',
109
+ inputSchema: {
110
+ project: z.string(),
111
+ dir: z.string().describe('Ruta absoluta del directorio a desplegar (para un frontend buildeado, la carpeta de salida)'),
112
+ message: z.string().optional(),
113
+ build: z.string().optional().describe('Comando a correr antes, ej: "npm run build"'),
114
+ },
115
+ }, wrap(async ({ project, dir, message, build }) => {
116
+ if (build) { const { execSync } = await import('node:child_process'); execSync(build, { cwd: dir, stdio: 'ignore' }); }
117
+ const tmpFile = path.join(tmpdir(), `alora-preview-${Date.now()}.tgz`);
118
+ await tar.create({ gzip: true, file: tmpFile, cwd: dir, filter: (p) => !p.split(path.sep).some((seg) => ['node_modules', '.git', 'data', '.DS_Store'].includes(seg)) }, ['.']);
119
+ const buffer = readFileSync(tmpFile);
120
+ rmSync(tmpFile, { force: true });
121
+ return client().preview(project, buffer, { message });
75
122
  }));
76
123
 
124
+ server.registerTool('promote', {
125
+ description: 'Promueve a producción el preview del proyecto (o una versión puntual). Lo que estaba en la URL temporal pasa a ser la versión viva; el preview se baja. Usalo cuando el humano aprobó lo que le mostraste con `preview`.',
126
+ inputSchema: { project: z.string(), version: z.number().optional().describe('Versión a promover; por default, la del preview activo') },
127
+ }, wrap(({ project, version }) => client().promote(project, version)));
128
+
129
+ server.registerTool('preview_stop', {
130
+ description: 'Baja el preview de un proyecto (la URL temporal deja de responder). La app viva no se toca.',
131
+ inputSchema: { project: z.string() },
132
+ }, wrap(({ project }) => client().previewStop(project)));
133
+
134
+ server.registerTool('preview_status', {
135
+ description: 'Muestra si hay un preview activo, su URL temporal, la versión y cuándo expira.',
136
+ inputSchema: { project: z.string() },
137
+ }, wrap(({ project }) => client().previewInfo(project)));
138
+
77
139
  server.registerTool('project_status', {
78
140
  description: 'Estado de un proyecto: versión activa, si está corriendo, URL.',
79
141
  inputSchema: { project: z.string() },