alora-mcp 0.11.0 → 0.12.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/README.md CHANGED
@@ -36,13 +36,14 @@ Prefer the classic way? Create an account at [alora.build/register](https://alor
36
36
 
37
37
  Either way 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).
38
38
 
39
- ## What your agent can do (45 tools)
39
+ ## What your agent can do (51 tools)
40
40
 
41
41
  | | |
42
42
  |---|---|
43
43
  | **Ship** | `create_project` · `deploy` (Node, Python, static, or any command via Procfile/alora.json) · `list_versions` · `rollback` |
44
44
  | **Preview before prod** | `preview` → temporary URL at `myapp--preview.alora.page` with throwaway data · `promote` · `preview_stop` |
45
45
  | **Share** | apps are born private — `share` by email with roles, `set_visibility`, `revoke_access` |
46
+ | **Give away a copy** | `project_send_copy` — the other person gets an independent project in *their* account (code only: no data, secrets, members or Claude credential; Alora flags `.env`/`.db`-style files before sending) · `project_clone` for a copy in your own account · `pull` downloads a project's code to edit it |
46
47
  | **Run things** | `set_always_on` / `set_worker_mode` for 24/7 processes · `cron_create` for scheduled hits · `exec` · `restart` · `logs` |
47
48
  | **Configure** | `secrets_set` (encrypted, injected as env) · `domain_connect` · `egress_ip_allocate` (static egress IP) |
48
49
  | **Use AI inside the app** | `ai_enable` — the deployed app runs `claude -p` with the owner's own Claude account |
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "alora-mcp",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
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.11.0",
10
+ "alora-sdk": "^0.12.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, writeFileSync, mkdirSync } from 'node:fs';
7
+ import { readFileSync, rmSync, writeFileSync, mkdirSync, existsSync, readdirSync } 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.10.0' });
21
+ const server = new McpServer({ name: 'alora', version: '0.12.0' });
22
22
 
23
23
  const ok = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
24
24
  const asError = (e) => ({
@@ -236,6 +236,53 @@ server.registerTool('set_visibility', {
236
236
  inputSchema: { project: z.string(), visibility: z.enum(['private', 'public']) },
237
237
  }, wrap(({ project, visibility }) => client().setVisibility(project, visibility)));
238
238
 
239
+ // --- copias: darle a alguien SU propia copia / clonar / bajar el código ---
240
+ server.registerTool('project_send_copy', {
241
+ description: 'Le da a OTRA persona su propia copia de un proyecto: un proyecto independiente en SU cuenta, con su saldo, sus datos y su propio Claude — el remitente no ve nada de la copia (a diferencia de "share", que solo deja USAR tu app). Viaja SOLO el código de una versión (la activa por default): nunca el volumen de datos, secrets, miembros, crons, always-on ni la credencial de Claude; la respuesta lista lo que quedó afuera (left_out) para avisarle al humano qué va a necesitar configurar el otro. Alora escanea el bundle antes: si trae archivos que no deberían viajar (.env, *.db/*.sqlite, claves, exports, carpetas "persist" de alora.json) responde review_needed con la lista y NO envía nada — miralos y reenviá con confirm=true solo si son parte legítima del código. La copia aparece sola en el dashboard y list_projects de la otra persona como proyecto propio, llamada <proyecto>-<usuario-del-email> salvo que pases name: al instante si ya tiene cuenta verificada, o cuando cree una con ese email (7 días).',
242
+ inputSchema: {
243
+ project: z.string(),
244
+ email: z.string().describe('Email real de quien recibe la copia (preguntáselo al humano)'),
245
+ name: z.string().optional().describe('Nombre del proyecto nuevo en la cuenta del otro (minúsculas, números, guiones). Default: <proyecto>-<usuario del email>'),
246
+ version: z.number().optional().describe('Versión a copiar. Default: la activa'),
247
+ confirm: z.boolean().optional().describe('true para reenviar después de un review_needed, habiendo mirado los archivos listados'),
248
+ },
249
+ }, wrap(({ project, email, name, version, confirm }) => client().sendCopy(project, { email, name, version, confirm })));
250
+
251
+ server.registerTool('project_clone', {
252
+ description: 'Copia un proyecto DENTRO de tu propia cuenta como proyecto nuevo e independiente (variante, staging, "la v2"): solo el código de una versión, sin datos, secrets, miembros ni crons. Inmediato.',
253
+ inputSchema: {
254
+ project: z.string(),
255
+ name: z.string().describe('Nombre del proyecto nuevo (tiene que estar libre)'),
256
+ version: z.number().optional().describe('Versión a copiar. Default: la activa'),
257
+ },
258
+ }, wrap(({ project, name, version }) => client().clone(project, name, { version })));
259
+
260
+ server.registerTool('pull', {
261
+ description: 'Baja el código de un proyecto (una versión; la activa por default) a un directorio local para modificarlo y volver a deployar. Es cómo se trabaja sobre una copia que le enviaron al humano, o cómo se recupera el código si el directorio local se perdió. Después: editar y deploy al mismo proyecto; project_status.copied_from dice qué le falta (secrets por nombre, IA, crons).',
262
+ inputSchema: {
263
+ project: z.string(),
264
+ dir: z.string().describe('Ruta absoluta del directorio destino (se crea; tiene que estar vacío salvo force=true)'),
265
+ version: z.number().optional(),
266
+ force: z.boolean().optional().describe('Extraer aunque el directorio tenga cosas (pisa archivos con el mismo nombre)'),
267
+ },
268
+ }, wrap(async ({ project, dir, version, force }) => {
269
+ if (existsSync(dir) && readdirSync(dir).length && !force) {
270
+ throw new AloraError({ error: 'dir_not_empty', message: `${dir} no está vacío.`, fix: 'Elegí otro directorio o pasá force=true para extraer encima.' }, 400);
271
+ }
272
+ const buf = await client().pull(project, { version });
273
+ mkdirSync(dir, { recursive: true });
274
+ const tmpFile = path.join(tmpdir(), `alora-pull-${Date.now()}.tgz`);
275
+ writeFileSync(tmpFile, buf);
276
+ try { await tar.x({ file: tmpFile, cwd: dir }); } finally { rmSync(tmpFile, { force: true }); }
277
+ const status = await client().status(project).catch(() => null);
278
+ return {
279
+ project, version: version || status?.active_version || null, dir, bytes: buf.length,
280
+ files: readdirSync(dir).length,
281
+ ...(status?.copied_from ? { copied_from: status.copied_from } : {}),
282
+ note: `Código en ${dir}. Editá y volvé a subir con deploy (project "${project}", dir "${dir}").${status?.copied_from ? ' Este proyecto nació como copia: copied_from.needs lista los secrets (por nombre), IA y crons que el original tenía y hay que reconstruir.' : ''}`,
283
+ };
284
+ }));
285
+
239
286
  server.registerTool('analytics_summary', {
240
287
  description: 'El pulso del proyecto: requests, usuarios activos, errores 5xx, rutas más usadas y serie por día. Usalo para saber qué partes de la app se usan (y cuáles no) antes de iterar.',
241
288
  inputSchema: { project: z.string(), period: z.enum(['24h', '7d', '30d']).optional() },
@@ -272,7 +319,7 @@ server.registerTool('ai_check', {
272
319
  }, wrap(({ project }) => client().aiCheck(project)));
273
320
 
274
321
  server.registerTool('ai_enable', {
275
- description: 'Habilita IA en un proyecto (Node o Python): la microVM recibe la credencial de Claude del dueño y el CLI `claude` queda en el PATH — la app puede ejecutar `claude -p "..." --output-format json` (usá child_process/subprocess). Requiere cuenta conectada (el humano, en su terminal, corre `npx alora-cli ai connect --setup` una vez: es interactivo — abre el navegador y solo él puede hacerlo). Si después claude falla en la app, ai_check dice por qué (cuota agotada ≠ desconectado). El primer arranque instala el CLI (~1 min); después es instantáneo. Con suscripción: solo para uso personal/bajo — el rate limit se comparte con el uso interactivo del dueño; para tráfico real conviene API key.',
322
+ description: 'Habilita IA en un proyecto (Node o Python): la microVM recibe la credencial de Claude del dueño y el CLI `claude` queda en el PATH — la app puede ejecutar `claude -p "..." --output-format json` (usá child_process/subprocess). Requiere cuenta conectada: corré VOS `npx alora-cli ai connect --setup` (Bash) en la compu del usuario se le abre el navegador y solo toca Authorize, sin pegar nada; esperá hasta 5 min. Alternativa: el humano corre ese mismo comando en su terminal. Si después claude falla en la app, ai_check dice por qué (cuota agotada ≠ desconectado). El primer arranque instala el CLI (~1 min); después es instantáneo. Con suscripción: solo para uso personal/bajo — el rate limit se comparte con el uso interactivo del dueño; para tráfico real conviene API key.',
276
323
  inputSchema: {
277
324
  project: z.string(),
278
325
  model: z.string().optional().describe('Modelo para claude dentro de la app (alias o id que acepte el CLI, ej "opus"). Sin esto, el default del CLI. Queda como ANTHROPIC_MODEL en el env.'),