@velixir/mcp 0.3.0 → 0.3.1
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 +1 -1
- package/package.json +1 -1
- package/src/index.js +37 -7
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ Point the editor's MCP settings at the same command (`npx -y @velixir/mcp`).
|
|
|
39
39
|
| --- | --- | --- |
|
|
40
40
|
| `directory` | yes | Absolute path to the app's source directory. |
|
|
41
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.
|
|
42
|
+
| `language` | no | node, python, go, ruby, php, java, elixir, rust, dotnet, deno, fsharp. Detected from the project files if omitted; pass it when the folder mixes languages. |
|
|
43
43
|
| `port` | no | Only if the app hard-codes a port instead of reading the `PORT` env var. |
|
|
44
44
|
|
|
45
45
|
It returns the live URL, a claim URL (give this to the user), when the claim window closes, a **deployment ID** (pass it to `logs`), and a status URL to poll.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velixir/mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Deploy web apps in any language to Velixir (the European cloud) straight from your AI agent: point deploy at a directory for a live URL and claim link, then read logs to debug what you shipped.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/index.js
CHANGED
|
@@ -30,22 +30,49 @@ const IGNORED_DIRS = new Set([
|
|
|
30
30
|
".venv", "venv", "__pycache__", ".pytest_cache", ".gradle", ".idea", ".vs",
|
|
31
31
|
]);
|
|
32
32
|
|
|
33
|
+
// Folders that hold credentials, never source.
|
|
34
|
+
const SECRET_DIRS = new Set([".ssh", ".aws", ".gnupg", ".docker", ".kube", ".azure"]);
|
|
35
|
+
|
|
36
|
+
// Files that hold secrets. An agent deploys whatever folder it was working in, and that folder
|
|
37
|
+
// usually has the user's .env in it. An unclaimed deploy is public for its whole window, so a
|
|
38
|
+
// secret uploaded here is a secret leaked. Example files (.env.example) are kept: they document
|
|
39
|
+
// variables without values and some apps read them at start-up.
|
|
40
|
+
const SECRET_FILE = [
|
|
41
|
+
/^\.env$/i,
|
|
42
|
+
/^\.env\.(?!example$|sample$|template$|dist$|defaults$).+/i,
|
|
43
|
+
/\.(pem|key|p12|pfx|jks|keystore|kdbx|ppk)$/i,
|
|
44
|
+
/^id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/i,
|
|
45
|
+
/^\.(npmrc|pypirc|netrc|git-credentials|pgpass)$/i,
|
|
46
|
+
/^(credentials|service[-_]?account.*)\.json$/i,
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
function isSecret(segments) {
|
|
50
|
+
const file = segments[segments.length - 1] ?? "";
|
|
51
|
+
return segments.some((seg) => SECRET_DIRS.has(seg)) || SECRET_FILE.some((re) => re.test(file));
|
|
52
|
+
}
|
|
53
|
+
|
|
33
54
|
async function tarballOf(dir) {
|
|
55
|
+
const skippedSecrets = [];
|
|
34
56
|
const stream = tarCreate(
|
|
35
57
|
{
|
|
36
58
|
gzip: true,
|
|
37
59
|
cwd: dir,
|
|
38
60
|
portable: true,
|
|
39
61
|
filter: (entryPath) => {
|
|
40
|
-
const segments = entryPath.split(/[\\/]/).filter(
|
|
41
|
-
|
|
62
|
+
const segments = entryPath.split(/[\\/]/).filter((s) => s && s !== ".");
|
|
63
|
+
if (segments.some((seg) => IGNORED_DIRS.has(seg))) return false;
|
|
64
|
+
if (isSecret(segments)) {
|
|
65
|
+
skippedSecrets.push(segments.join("/"));
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
42
69
|
},
|
|
43
70
|
},
|
|
44
71
|
["."],
|
|
45
72
|
);
|
|
46
73
|
const chunks = [];
|
|
47
74
|
for await (const chunk of stream) chunks.push(chunk);
|
|
48
|
-
return Buffer.concat(chunks);
|
|
75
|
+
return { buffer: Buffer.concat(chunks), skippedSecrets };
|
|
49
76
|
}
|
|
50
77
|
|
|
51
78
|
// Anonymous reads are rate limited per client (a burst, then one every two seconds). Tell the
|
|
@@ -55,7 +82,7 @@ function rateLimited(res) {
|
|
|
55
82
|
return { isError: true, content: [{ type: "text", text: `Rate limited by Velixir. Wait about ${wait} seconds before calling this again, and avoid polling in a tight loop.` }] };
|
|
56
83
|
}
|
|
57
84
|
|
|
58
|
-
const server = new McpServer({ name: "velixir", version: "0.3.
|
|
85
|
+
const server = new McpServer({ name: "velixir", version: "0.3.1" });
|
|
59
86
|
|
|
60
87
|
server.tool(
|
|
61
88
|
"deploy",
|
|
@@ -71,7 +98,7 @@ server.tool(
|
|
|
71
98
|
language: z
|
|
72
99
|
.string()
|
|
73
100
|
.optional()
|
|
74
|
-
.describe("Primary language (node, python, go, ruby, php, java, elixir, rust, dotnet, deno, fsharp).
|
|
101
|
+
.describe("Primary language (node, python, go, ruby, php, java, elixir, rust, dotnet, deno, fsharp). Detected from the project files (package.json, go.mod, *.csproj and so on) if omitted; pass it when the folder mixes languages."),
|
|
75
102
|
port: z
|
|
76
103
|
.number()
|
|
77
104
|
.int()
|
|
@@ -85,9 +112,9 @@ server.tool(
|
|
|
85
112
|
return { isError: true, content: [{ type: "text", text: `Not a directory: ${dir}` }] };
|
|
86
113
|
}
|
|
87
114
|
|
|
88
|
-
let tar;
|
|
115
|
+
let tar, skippedSecrets;
|
|
89
116
|
try {
|
|
90
|
-
tar = await tarballOf(dir);
|
|
117
|
+
({ buffer: tar, skippedSecrets } = await tarballOf(dir));
|
|
91
118
|
} catch (err) {
|
|
92
119
|
return { isError: true, content: [{ type: "text", text: `Could not package the source: ${err?.message ?? err}` }] };
|
|
93
120
|
}
|
|
@@ -124,6 +151,9 @@ server.tool(
|
|
|
124
151
|
? `Deployment ID: ${data.deployment_id}\n(Use the "logs" tool with this ID to see build/runtime logs and debug if the app doesn't come up.)`
|
|
125
152
|
: null,
|
|
126
153
|
data.status_url ? `Deployment status: ${data.status_url}` : null,
|
|
154
|
+
skippedSecrets.length
|
|
155
|
+
? `Not uploaded, because they look like secrets: ${skippedSecrets.slice(0, 10).join(", ")}${skippedSecrets.length > 10 ? ` and ${skippedSecrets.length - 10} more` : ""}. If the app needs these values, the user can set them as environment variables after claiming it.`
|
|
156
|
+
: null,
|
|
127
157
|
].filter(Boolean);
|
|
128
158
|
|
|
129
159
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|