@velixir/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/README.md +56 -0
- package/package.json +32 -0
- package/src/index.js +113 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @velixir/mcp
|
|
2
|
+
|
|
3
|
+
Deploy web apps in **any language** to [Velixir](https://velixir.net), the European cloud, straight from your AI agent. One tool, `deploy`: point it at a source directory, and it packs the code, builds it with no Dockerfile, and returns a live URL plus a claim link the human opens to keep the app on a free, card-free account. No Velixir account is needed to deploy.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
The agent generated a real backend. Now it has to run somewhere. Velixir runs real apps in Node, Python, Go, Ruby, PHP, Java, Elixir, Rust, .NET, and more, with managed Postgres and Valkey, in the EU, with no egress fees. This server lets an agent one-shot deploy the app it just built, then hand the user a link to keep it.
|
|
8
|
+
|
|
9
|
+
## Use it
|
|
10
|
+
|
|
11
|
+
Zero install, run with npx:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx -y @velixir/mcp
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Claude Desktop / Claude Code
|
|
18
|
+
|
|
19
|
+
Add to your MCP config:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"velixir": {
|
|
25
|
+
"command": "npx",
|
|
26
|
+
"args": ["-y", "@velixir/mcp"]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Cursor / Cline / Windsurf
|
|
33
|
+
|
|
34
|
+
Point the editor's MCP settings at the same command (`npx -y @velixir/mcp`).
|
|
35
|
+
|
|
36
|
+
## The `deploy` tool
|
|
37
|
+
|
|
38
|
+
| Input | Required | Description |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| `directory` | yes | Absolute path to the app's source directory. |
|
|
41
|
+
| `name` | no | Short name for the app; becomes part of the URL. |
|
|
42
|
+
| `language` | no | node, python, go, ruby, php, java, elixir, rust, dotnet, deno, fsharp. Auto-detected if omitted. |
|
|
43
|
+
|
|
44
|
+
It returns the live URL, a claim URL (give this to the user), when the claim window closes, and a status URL to poll.
|
|
45
|
+
|
|
46
|
+
`node_modules`, `.git`, build output, and caches are skipped automatically. Nixpacks reinstalls dependencies and builds from source.
|
|
47
|
+
|
|
48
|
+
## Config
|
|
49
|
+
|
|
50
|
+
| Env var | Default | Description |
|
|
51
|
+
| --- | --- | --- |
|
|
52
|
+
| `VELIXIR_API_URL` | `https://velixir.net` | Override for staging or self-host. |
|
|
53
|
+
|
|
54
|
+
## Requirements
|
|
55
|
+
|
|
56
|
+
Node.js 18 or newer.
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@velixir/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Deploy web apps in any language to Velixir (the European cloud) straight from your AI agent. One tool: point it at a directory, get back a live URL and a claim link.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"velixir-mcp": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"modelcontextprotocol",
|
|
19
|
+
"velixir",
|
|
20
|
+
"deploy",
|
|
21
|
+
"paas",
|
|
22
|
+
"agent"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"homepage": "https://velixir.ai",
|
|
26
|
+
"publishConfig": { "access": "public" },
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.10.0",
|
|
29
|
+
"tar": "^7.4.3",
|
|
30
|
+
"zod": "^3.23.8"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Velixir MCP server. Exposes one tool, `deploy`, that packs a source directory,
|
|
3
|
+
// posts it to Velixir's account-less deploy endpoint (POST /api/v1/deploy), and
|
|
4
|
+
// returns a live URL plus a single-use claim link the human opens to keep the app
|
|
5
|
+
// on a free, card-free account. No Velixir account is needed to deploy.
|
|
6
|
+
//
|
|
7
|
+
// Zero setup: run with `npx -y @velixir/mcp`. Point VELIXIR_API_URL at a different
|
|
8
|
+
// host for staging/self-host (default https://velixir.net).
|
|
9
|
+
|
|
10
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { create as tarCreate } from "tar";
|
|
14
|
+
import { promises as fs } from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
const API_URL = (process.env.VELIXIR_API_URL || "https://velixir.net").replace(/\/+$/, "");
|
|
18
|
+
|
|
19
|
+
// Directory names never worth uploading: build output, dependencies, VCS, caches.
|
|
20
|
+
// Nixpacks reinstalls dependencies and rebuilds from source, so shipping these
|
|
21
|
+
// would only bloat the upload.
|
|
22
|
+
const IGNORED_DIRS = new Set([
|
|
23
|
+
"node_modules", ".git", ".hg", ".svn", ".next", ".nuxt", "dist", "build",
|
|
24
|
+
"out", ".turbo", ".cache", ".parcel-cache", "target", "bin", "obj",
|
|
25
|
+
".venv", "venv", "__pycache__", ".pytest_cache", ".gradle", ".idea", ".vs",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
async function tarballOf(dir) {
|
|
29
|
+
const stream = tarCreate(
|
|
30
|
+
{
|
|
31
|
+
gzip: true,
|
|
32
|
+
cwd: dir,
|
|
33
|
+
portable: true,
|
|
34
|
+
filter: (entryPath) => {
|
|
35
|
+
const segments = entryPath.split(/[\\/]/).filter(Boolean);
|
|
36
|
+
return !segments.some((seg) => IGNORED_DIRS.has(seg));
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
["."],
|
|
40
|
+
);
|
|
41
|
+
const chunks = [];
|
|
42
|
+
for await (const chunk of stream) chunks.push(chunk);
|
|
43
|
+
return Buffer.concat(chunks);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const server = new McpServer({ name: "velixir", version: "0.1.0" });
|
|
47
|
+
|
|
48
|
+
server.tool(
|
|
49
|
+
"deploy",
|
|
50
|
+
"Deploy a web app in ANY language (Node, Python, Go, Ruby, PHP, Java, Elixir, Rust, .NET, and more) to Velixir, the European cloud. Packs the given source directory, builds it with no Dockerfile, and runs it in the EU. Returns a live URL plus a claim link the human opens to keep the app on a free, card-free account. No Velixir account is needed to deploy. Use this when the user wants to publish, host, or deploy an app you generated.",
|
|
51
|
+
{
|
|
52
|
+
directory: z
|
|
53
|
+
.string()
|
|
54
|
+
.describe("Absolute path to the app's source directory to deploy (the folder that contains its package.json / requirements.txt / go.mod / etc)."),
|
|
55
|
+
name: z
|
|
56
|
+
.string()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe("A short name for the app; becomes part of the URL. Letters and numbers work best."),
|
|
59
|
+
language: z
|
|
60
|
+
.string()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe("Primary language (node, python, go, ruby, php, java, elixir, rust, dotnet, deno, fsharp). Auto-detected from the source if omitted."),
|
|
63
|
+
},
|
|
64
|
+
async ({ directory, name, language }) => {
|
|
65
|
+
const dir = path.resolve(directory);
|
|
66
|
+
const stat = await fs.stat(dir).catch(() => null);
|
|
67
|
+
if (!stat || !stat.isDirectory()) {
|
|
68
|
+
return { isError: true, content: [{ type: "text", text: `Not a directory: ${dir}` }] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let tar;
|
|
72
|
+
try {
|
|
73
|
+
tar = await tarballOf(dir);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
return { isError: true, content: [{ type: "text", text: `Could not package the source: ${err?.message ?? err}` }] };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const form = new FormData();
|
|
79
|
+
form.append("source", new Blob([tar], { type: "application/gzip" }), "source.tar.gz");
|
|
80
|
+
if (name) form.append("name", name);
|
|
81
|
+
if (language) form.append("language", language);
|
|
82
|
+
|
|
83
|
+
let res, body;
|
|
84
|
+
try {
|
|
85
|
+
res = await fetch(`${API_URL}/api/v1/deploy`, { method: "POST", body: form });
|
|
86
|
+
body = await res.text();
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return { isError: true, content: [{ type: "text", text: `Could not reach Velixir at ${API_URL}: ${err?.message ?? err}` }] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
return { isError: true, content: [{ type: "text", text: `Deploy failed (HTTP ${res.status}): ${body}` }] };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let data;
|
|
96
|
+
try { data = JSON.parse(body); } catch { data = {}; }
|
|
97
|
+
|
|
98
|
+
const lines = [
|
|
99
|
+
"Your app is building on Velixir now.",
|
|
100
|
+
data.app_url ? `Live URL: ${data.app_url}` : null,
|
|
101
|
+
data.claim_url
|
|
102
|
+
? `Claim URL: ${data.claim_url}\n(Give this to the user. Opening it keeps the app on a free, card-free Velixir account; unclaimed apps are removed after a short window.)`
|
|
103
|
+
: null,
|
|
104
|
+
data.claim_expires_at ? `Claim before: ${data.claim_expires_at}` : null,
|
|
105
|
+
data.status_url ? `Deployment status: ${data.status_url}` : null,
|
|
106
|
+
].filter(Boolean);
|
|
107
|
+
|
|
108
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const transport = new StdioServerTransport();
|
|
113
|
+
await server.connect(transport);
|