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.
- package/README.md +62 -0
- package/package.json +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.
|
|
4
|
-
"description": "
|
|
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.
|
|
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.
|
|
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() },
|