helipod-mcp 0.3.0 → 0.4.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 +5 -5
- package/dist/http.js +33 -22
- package/dist/index.js +33 -22
- package/dist/stdio.js +34 -23
- package/package.json +1 -1
- package/skills/helipod/SKILL.md +1 -1
- package/skills/helipod-email/SKILL.md +2 -2
package/README.md
CHANGED
|
@@ -27,11 +27,11 @@ Prefer a key, a local server, or a tool not listed? Read on.
|
|
|
27
27
|
|
|
28
28
|
In the [dashboard](https://helipod.io/project) open a project, then **Settings → API keys**. Choose the products (Container API, Database API, VPS API, Domain API) and the reach (one project, or the whole account; the Domain API needs a whole-account key). The key starts with `hel_`.
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
For the email tools, enable **Email API** on the key (or sign in with it ticked). An email key from the project's **Email** section also still works, as `HELIPOD_EMAIL_API_KEY`.
|
|
31
31
|
|
|
32
32
|
```sh
|
|
33
33
|
export HELIPOD_API_KEY=hel_...
|
|
34
|
-
export HELIPOD_EMAIL_API_KEY=... # optional
|
|
34
|
+
export HELIPOD_EMAIL_API_KEY=... # optional: only if you use a separate email key
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Start with a key limited to one project while you try things out.
|
|
@@ -150,11 +150,11 @@ Gemini CLI only starts MCP servers in folders you have trusted.
|
|
|
150
150
|
|
|
151
151
|
### Other clients
|
|
152
152
|
|
|
153
|
-
Run `npx -y helipod-mcp` as a stdio MCP server with `HELIPOD_API_KEY` (
|
|
153
|
+
Run `npx -y helipod-mcp` as a stdio MCP server with `HELIPOD_API_KEY` in its environment (enable the Email API on it for the email tools, or add `HELIPOD_EMAIL_API_KEY`).
|
|
154
154
|
|
|
155
155
|
## Hosted server (nothing to install)
|
|
156
156
|
|
|
157
|
-
Clients that connect by URL can use `https://api.helipod.io/mcp` (Streamable HTTP) with `Authorization: Bearer hel_...` (
|
|
157
|
+
Clients that connect by URL can use `https://api.helipod.io/mcp` (Streamable HTTP) with `Authorization: Bearer hel_...` (the same key or sign-in serves the email tools when it has the Email API enabled; `X-Helipod-Email-Key` still works). Same tools; the key is used per request and never stored.
|
|
158
158
|
|
|
159
159
|
```sh
|
|
160
160
|
claude mcp add --transport http helipod https://api.helipod.io/mcp --header "Authorization: Bearer $HELIPOD_API_KEY"
|
|
@@ -211,7 +211,7 @@ Each tool is marked read-only, write or destructive, so clients can ask before r
|
|
|
211
211
|
| Variable | Default |
|
|
212
212
|
|---|---|
|
|
213
213
|
| `HELIPOD_API_KEY` | (required for containers, VPS, domains) |
|
|
214
|
-
| `HELIPOD_EMAIL_API_KEY` | (
|
|
214
|
+
| `HELIPOD_EMAIL_API_KEY` | (optional; the email tools use `HELIPOD_API_KEY` when it has the Email API enabled) |
|
|
215
215
|
| `HELIPOD_API_URL` | `https://api.helipod.io/api` |
|
|
216
216
|
| `HELIPOD_EMAIL_API_URL` | `https://mail.helipod.io/api/email` |
|
|
217
217
|
|
package/dist/http.js
CHANGED
|
@@ -35614,19 +35614,28 @@ class HelipodClient {
|
|
|
35614
35614
|
return this.request(this.apiUrl, opts.auth === false ? undefined : this.cfg.apiKey, method, path, opts);
|
|
35615
35615
|
}
|
|
35616
35616
|
email(method, path, opts = {}) {
|
|
35617
|
-
|
|
35618
|
-
|
|
35617
|
+
const key = this.cfg.emailApiKey || this.cfg.apiKey;
|
|
35618
|
+
if (!key) {
|
|
35619
|
+
throw new HelipodError(401, "NO_API_KEY", "No key set. Use HELIPOD_API_KEY with the Email API enabled (or HELIPOD_EMAIL_API_KEY). Create keys in the Helipod dashboard: Project → Settings → API keys.");
|
|
35619
35620
|
}
|
|
35620
|
-
|
|
35621
|
+
const headers = !this.cfg.emailApiKey && opts.project ? { "x-helipod-project": opts.project } : undefined;
|
|
35622
|
+
return this.request(this.emailApiUrl, key, method, path, { ...opts, headers });
|
|
35621
35623
|
}
|
|
35622
35624
|
async request(base, key, method, path, opts) {
|
|
35623
|
-
const
|
|
35625
|
+
const raw = path.startsWith("/") ? path : `/${path}`;
|
|
35626
|
+
if (/(^|\/)\.\.?(\/|$)|[?#\\]|\/\/|%2e|%2f|%5c/i.test(raw)) {
|
|
35627
|
+
throw new HelipodError(400, "INVALID_PATH", "An id in this request contains characters that are not allowed.");
|
|
35628
|
+
}
|
|
35629
|
+
const url2 = new URL(base + raw);
|
|
35624
35630
|
for (const [k, v] of Object.entries(opts.query ?? {}))
|
|
35625
35631
|
if (v !== undefined && v !== null && v !== "")
|
|
35626
35632
|
url2.searchParams.set(k, String(v));
|
|
35627
35633
|
const headers = { accept: "application/json", "user-agent": this.cfg.userAgent ?? "helipod-mcp" };
|
|
35628
35634
|
if (key)
|
|
35629
35635
|
headers.authorization = `Bearer ${key}`;
|
|
35636
|
+
for (const [k, v] of Object.entries(opts.headers ?? {}))
|
|
35637
|
+
if (/^[0-9a-f-]{36}$/i.test(v))
|
|
35638
|
+
headers[k] = v;
|
|
35630
35639
|
if (opts.body !== undefined)
|
|
35631
35640
|
headers["content-type"] = "application/json";
|
|
35632
35641
|
if (opts.idempotencyKey)
|
|
@@ -36322,14 +36331,15 @@ var domainTools = [
|
|
|
36322
36331
|
];
|
|
36323
36332
|
|
|
36324
36333
|
// src/tools/email.ts
|
|
36334
|
+
var projectId = exports_external.string().uuid().optional().describe("Project whose email to use. Only needed with a whole-account key that reaches several projects with email.");
|
|
36325
36335
|
var emailTools = [
|
|
36326
36336
|
defineTool({
|
|
36327
36337
|
name: "email_status",
|
|
36328
36338
|
title: "Email status",
|
|
36329
36339
|
kind: "read",
|
|
36330
36340
|
description: "Email service of the key's project: plan, usage today and this month, limits, counts.",
|
|
36331
|
-
input: {},
|
|
36332
|
-
run: (
|
|
36341
|
+
input: { projectId },
|
|
36342
|
+
run: (a, api2) => api2.email("GET", "/status", { project: a.projectId })
|
|
36333
36343
|
}),
|
|
36334
36344
|
defineTool({
|
|
36335
36345
|
name: "send_email",
|
|
@@ -36337,6 +36347,7 @@ var emailTools = [
|
|
|
36337
36347
|
kind: "write",
|
|
36338
36348
|
description: "Send one email from a verified domain of the project (counts against the plan's quota). Use templateId + templateData, or subject + html/text. Pass an idempotencyKey when retrying.",
|
|
36339
36349
|
input: {
|
|
36350
|
+
projectId,
|
|
36340
36351
|
from: exports_external.string().describe('"hello@example.com" or "Acme <hello@example.com>"'),
|
|
36341
36352
|
to: exports_external.array(exports_external.string()).min(1).max(50),
|
|
36342
36353
|
cc: exports_external.array(exports_external.string()).max(50).optional(),
|
|
@@ -36350,63 +36361,63 @@ var emailTools = [
|
|
|
36350
36361
|
variables: exports_external.record(exports_external.string(), exports_external.string()).optional(),
|
|
36351
36362
|
idempotencyKey: exports_external.string().max(128).optional()
|
|
36352
36363
|
},
|
|
36353
|
-
run: (a, api2) => api2.email("POST", "/send", { body: a })
|
|
36364
|
+
run: ({ projectId: project, ...a }, api2) => api2.email("POST", "/send", { body: a, project })
|
|
36354
36365
|
}),
|
|
36355
36366
|
defineTool({
|
|
36356
36367
|
name: "list_emails",
|
|
36357
36368
|
title: "List sent emails",
|
|
36358
36369
|
kind: "read",
|
|
36359
36370
|
description: "Email log, newest first, with delivery status.",
|
|
36360
|
-
input: { status: exports_external.string().optional(), search: exports_external.string().optional(), limit: exports_external.number().int().min(1).max(100).optional(), cursor: exports_external.string().optional() },
|
|
36361
|
-
run: (a, api2) => api2.email("GET", "/emails", { query: a })
|
|
36371
|
+
input: { projectId, status: exports_external.string().optional(), search: exports_external.string().optional(), limit: exports_external.number().int().min(1).max(100).optional(), cursor: exports_external.string().optional() },
|
|
36372
|
+
run: ({ projectId: project, ...a }, api2) => api2.email("GET", "/emails", { query: a, project })
|
|
36362
36373
|
}),
|
|
36363
36374
|
defineTool({
|
|
36364
36375
|
name: "get_email",
|
|
36365
36376
|
title: "Get email",
|
|
36366
36377
|
kind: "read",
|
|
36367
36378
|
description: "One sent email with its status and events.",
|
|
36368
|
-
input: { emailId: exports_external.string() },
|
|
36369
|
-
run: (a, api2) => api2.email("GET", `/emails/${a.emailId}
|
|
36379
|
+
input: { projectId, emailId: exports_external.string() },
|
|
36380
|
+
run: (a, api2) => api2.email("GET", `/emails/${a.emailId}`, { project: a.projectId })
|
|
36370
36381
|
}),
|
|
36371
36382
|
defineTool({
|
|
36372
36383
|
name: "list_email_domains",
|
|
36373
36384
|
title: "List sending domains",
|
|
36374
36385
|
kind: "read",
|
|
36375
36386
|
description: "Sending domains with verification status and the DNS records to add.",
|
|
36376
|
-
input: {},
|
|
36377
|
-
run: (
|
|
36387
|
+
input: { projectId },
|
|
36388
|
+
run: (a, api2) => api2.email("GET", "/domains", { project: a.projectId })
|
|
36378
36389
|
}),
|
|
36379
36390
|
defineTool({
|
|
36380
36391
|
name: "add_email_domain",
|
|
36381
36392
|
title: "Add sending domain",
|
|
36382
36393
|
kind: "write",
|
|
36383
36394
|
description: "Add a sending domain; the response lists the DNS records the user must create, then call verify_email_domain.",
|
|
36384
|
-
input: { domain: exports_external.string() },
|
|
36385
|
-
run: (a, api2) => api2.email("POST", "/domains", { body: { domain: a.domain } })
|
|
36395
|
+
input: { projectId, domain: exports_external.string() },
|
|
36396
|
+
run: (a, api2) => api2.email("POST", "/domains", { body: { domain: a.domain }, project: a.projectId })
|
|
36386
36397
|
}),
|
|
36387
36398
|
defineTool({
|
|
36388
36399
|
name: "verify_email_domain",
|
|
36389
36400
|
title: "Verify sending domain",
|
|
36390
36401
|
kind: "write",
|
|
36391
36402
|
description: "Check a sending domain's DNS records again.",
|
|
36392
|
-
input: { domainId: exports_external.string() },
|
|
36393
|
-
run: (a, api2) => api2.email("POST", `/domains/${a.domainId}/verify
|
|
36403
|
+
input: { projectId, domainId: exports_external.string() },
|
|
36404
|
+
run: (a, api2) => api2.email("POST", `/domains/${a.domainId}/verify`, { project: a.projectId })
|
|
36394
36405
|
}),
|
|
36395
36406
|
defineTool({
|
|
36396
36407
|
name: "list_email_templates",
|
|
36397
36408
|
title: "List email templates",
|
|
36398
36409
|
kind: "read",
|
|
36399
36410
|
description: "Email templates of the project.",
|
|
36400
|
-
input: {},
|
|
36401
|
-
run: (
|
|
36411
|
+
input: { projectId },
|
|
36412
|
+
run: (a, api2) => api2.email("GET", "/templates", { project: a.projectId })
|
|
36402
36413
|
}),
|
|
36403
36414
|
defineTool({
|
|
36404
36415
|
name: "add_contact",
|
|
36405
36416
|
title: "Add contact",
|
|
36406
36417
|
kind: "write",
|
|
36407
36418
|
description: "Add or update an audience contact.",
|
|
36408
|
-
input: { email: exports_external.string(), firstName: exports_external.string().optional(), lastName: exports_external.string().optional() },
|
|
36409
|
-
run: (a, api2) => api2.email("POST", "/contacts", { body: a })
|
|
36419
|
+
input: { projectId, email: exports_external.string(), firstName: exports_external.string().optional(), lastName: exports_external.string().optional() },
|
|
36420
|
+
run: ({ projectId: project, ...a }, api2) => api2.email("POST", "/contacts", { body: a, project })
|
|
36410
36421
|
})
|
|
36411
36422
|
];
|
|
36412
36423
|
|
|
@@ -36609,7 +36620,7 @@ var generalTools = [
|
|
|
36609
36620
|
];
|
|
36610
36621
|
|
|
36611
36622
|
// src/server.ts
|
|
36612
|
-
var VERSION = "0.
|
|
36623
|
+
var VERSION = "0.4.0";
|
|
36613
36624
|
var INSTRUCTIONS = `Helipod is a cloud platform (Indonesia): containers, managed Postgres databases, VPS, domains, email and an AI gateway, billed from a prepaid wallet in Rupiah.
|
|
36614
36625
|
- Start with list_projects to find projects and environment ids.
|
|
36615
36626
|
- Anything that costs money (create/deploy/scale services, upgrade a database plan, create/renew/upgrade VPS, register/renew/transfer domains): run the dryRun or price lookup first, tell the user the cost in IDR and wait for an explicit yes.
|
package/dist/index.js
CHANGED
|
@@ -34759,19 +34759,28 @@ class HelipodClient {
|
|
|
34759
34759
|
return this.request(this.apiUrl, opts.auth === false ? undefined : this.cfg.apiKey, method, path, opts);
|
|
34760
34760
|
}
|
|
34761
34761
|
email(method, path, opts = {}) {
|
|
34762
|
-
|
|
34763
|
-
|
|
34762
|
+
const key = this.cfg.emailApiKey || this.cfg.apiKey;
|
|
34763
|
+
if (!key) {
|
|
34764
|
+
throw new HelipodError(401, "NO_API_KEY", "No key set. Use HELIPOD_API_KEY with the Email API enabled (or HELIPOD_EMAIL_API_KEY). Create keys in the Helipod dashboard: Project → Settings → API keys.");
|
|
34764
34765
|
}
|
|
34765
|
-
|
|
34766
|
+
const headers = !this.cfg.emailApiKey && opts.project ? { "x-helipod-project": opts.project } : undefined;
|
|
34767
|
+
return this.request(this.emailApiUrl, key, method, path, { ...opts, headers });
|
|
34766
34768
|
}
|
|
34767
34769
|
async request(base, key, method, path, opts) {
|
|
34768
|
-
const
|
|
34770
|
+
const raw = path.startsWith("/") ? path : `/${path}`;
|
|
34771
|
+
if (/(^|\/)\.\.?(\/|$)|[?#\\]|\/\/|%2e|%2f|%5c/i.test(raw)) {
|
|
34772
|
+
throw new HelipodError(400, "INVALID_PATH", "An id in this request contains characters that are not allowed.");
|
|
34773
|
+
}
|
|
34774
|
+
const url2 = new URL(base + raw);
|
|
34769
34775
|
for (const [k, v] of Object.entries(opts.query ?? {}))
|
|
34770
34776
|
if (v !== undefined && v !== null && v !== "")
|
|
34771
34777
|
url2.searchParams.set(k, String(v));
|
|
34772
34778
|
const headers = { accept: "application/json", "user-agent": this.cfg.userAgent ?? "helipod-mcp" };
|
|
34773
34779
|
if (key)
|
|
34774
34780
|
headers.authorization = `Bearer ${key}`;
|
|
34781
|
+
for (const [k, v] of Object.entries(opts.headers ?? {}))
|
|
34782
|
+
if (/^[0-9a-f-]{36}$/i.test(v))
|
|
34783
|
+
headers[k] = v;
|
|
34775
34784
|
if (opts.body !== undefined)
|
|
34776
34785
|
headers["content-type"] = "application/json";
|
|
34777
34786
|
if (opts.idempotencyKey)
|
|
@@ -35467,14 +35476,15 @@ var domainTools = [
|
|
|
35467
35476
|
];
|
|
35468
35477
|
|
|
35469
35478
|
// src/tools/email.ts
|
|
35479
|
+
var projectId = exports_external.string().uuid().optional().describe("Project whose email to use. Only needed with a whole-account key that reaches several projects with email.");
|
|
35470
35480
|
var emailTools = [
|
|
35471
35481
|
defineTool({
|
|
35472
35482
|
name: "email_status",
|
|
35473
35483
|
title: "Email status",
|
|
35474
35484
|
kind: "read",
|
|
35475
35485
|
description: "Email service of the key's project: plan, usage today and this month, limits, counts.",
|
|
35476
|
-
input: {},
|
|
35477
|
-
run: (
|
|
35486
|
+
input: { projectId },
|
|
35487
|
+
run: (a, api2) => api2.email("GET", "/status", { project: a.projectId })
|
|
35478
35488
|
}),
|
|
35479
35489
|
defineTool({
|
|
35480
35490
|
name: "send_email",
|
|
@@ -35482,6 +35492,7 @@ var emailTools = [
|
|
|
35482
35492
|
kind: "write",
|
|
35483
35493
|
description: "Send one email from a verified domain of the project (counts against the plan's quota). Use templateId + templateData, or subject + html/text. Pass an idempotencyKey when retrying.",
|
|
35484
35494
|
input: {
|
|
35495
|
+
projectId,
|
|
35485
35496
|
from: exports_external.string().describe('"hello@example.com" or "Acme <hello@example.com>"'),
|
|
35486
35497
|
to: exports_external.array(exports_external.string()).min(1).max(50),
|
|
35487
35498
|
cc: exports_external.array(exports_external.string()).max(50).optional(),
|
|
@@ -35495,63 +35506,63 @@ var emailTools = [
|
|
|
35495
35506
|
variables: exports_external.record(exports_external.string(), exports_external.string()).optional(),
|
|
35496
35507
|
idempotencyKey: exports_external.string().max(128).optional()
|
|
35497
35508
|
},
|
|
35498
|
-
run: (a, api2) => api2.email("POST", "/send", { body: a })
|
|
35509
|
+
run: ({ projectId: project, ...a }, api2) => api2.email("POST", "/send", { body: a, project })
|
|
35499
35510
|
}),
|
|
35500
35511
|
defineTool({
|
|
35501
35512
|
name: "list_emails",
|
|
35502
35513
|
title: "List sent emails",
|
|
35503
35514
|
kind: "read",
|
|
35504
35515
|
description: "Email log, newest first, with delivery status.",
|
|
35505
|
-
input: { status: exports_external.string().optional(), search: exports_external.string().optional(), limit: exports_external.number().int().min(1).max(100).optional(), cursor: exports_external.string().optional() },
|
|
35506
|
-
run: (a, api2) => api2.email("GET", "/emails", { query: a })
|
|
35516
|
+
input: { projectId, status: exports_external.string().optional(), search: exports_external.string().optional(), limit: exports_external.number().int().min(1).max(100).optional(), cursor: exports_external.string().optional() },
|
|
35517
|
+
run: ({ projectId: project, ...a }, api2) => api2.email("GET", "/emails", { query: a, project })
|
|
35507
35518
|
}),
|
|
35508
35519
|
defineTool({
|
|
35509
35520
|
name: "get_email",
|
|
35510
35521
|
title: "Get email",
|
|
35511
35522
|
kind: "read",
|
|
35512
35523
|
description: "One sent email with its status and events.",
|
|
35513
|
-
input: { emailId: exports_external.string() },
|
|
35514
|
-
run: (a, api2) => api2.email("GET", `/emails/${a.emailId}
|
|
35524
|
+
input: { projectId, emailId: exports_external.string() },
|
|
35525
|
+
run: (a, api2) => api2.email("GET", `/emails/${a.emailId}`, { project: a.projectId })
|
|
35515
35526
|
}),
|
|
35516
35527
|
defineTool({
|
|
35517
35528
|
name: "list_email_domains",
|
|
35518
35529
|
title: "List sending domains",
|
|
35519
35530
|
kind: "read",
|
|
35520
35531
|
description: "Sending domains with verification status and the DNS records to add.",
|
|
35521
|
-
input: {},
|
|
35522
|
-
run: (
|
|
35532
|
+
input: { projectId },
|
|
35533
|
+
run: (a, api2) => api2.email("GET", "/domains", { project: a.projectId })
|
|
35523
35534
|
}),
|
|
35524
35535
|
defineTool({
|
|
35525
35536
|
name: "add_email_domain",
|
|
35526
35537
|
title: "Add sending domain",
|
|
35527
35538
|
kind: "write",
|
|
35528
35539
|
description: "Add a sending domain; the response lists the DNS records the user must create, then call verify_email_domain.",
|
|
35529
|
-
input: { domain: exports_external.string() },
|
|
35530
|
-
run: (a, api2) => api2.email("POST", "/domains", { body: { domain: a.domain } })
|
|
35540
|
+
input: { projectId, domain: exports_external.string() },
|
|
35541
|
+
run: (a, api2) => api2.email("POST", "/domains", { body: { domain: a.domain }, project: a.projectId })
|
|
35531
35542
|
}),
|
|
35532
35543
|
defineTool({
|
|
35533
35544
|
name: "verify_email_domain",
|
|
35534
35545
|
title: "Verify sending domain",
|
|
35535
35546
|
kind: "write",
|
|
35536
35547
|
description: "Check a sending domain's DNS records again.",
|
|
35537
|
-
input: { domainId: exports_external.string() },
|
|
35538
|
-
run: (a, api2) => api2.email("POST", `/domains/${a.domainId}/verify
|
|
35548
|
+
input: { projectId, domainId: exports_external.string() },
|
|
35549
|
+
run: (a, api2) => api2.email("POST", `/domains/${a.domainId}/verify`, { project: a.projectId })
|
|
35539
35550
|
}),
|
|
35540
35551
|
defineTool({
|
|
35541
35552
|
name: "list_email_templates",
|
|
35542
35553
|
title: "List email templates",
|
|
35543
35554
|
kind: "read",
|
|
35544
35555
|
description: "Email templates of the project.",
|
|
35545
|
-
input: {},
|
|
35546
|
-
run: (
|
|
35556
|
+
input: { projectId },
|
|
35557
|
+
run: (a, api2) => api2.email("GET", "/templates", { project: a.projectId })
|
|
35547
35558
|
}),
|
|
35548
35559
|
defineTool({
|
|
35549
35560
|
name: "add_contact",
|
|
35550
35561
|
title: "Add contact",
|
|
35551
35562
|
kind: "write",
|
|
35552
35563
|
description: "Add or update an audience contact.",
|
|
35553
|
-
input: { email: exports_external.string(), firstName: exports_external.string().optional(), lastName: exports_external.string().optional() },
|
|
35554
|
-
run: (a, api2) => api2.email("POST", "/contacts", { body: a })
|
|
35564
|
+
input: { projectId, email: exports_external.string(), firstName: exports_external.string().optional(), lastName: exports_external.string().optional() },
|
|
35565
|
+
run: ({ projectId: project, ...a }, api2) => api2.email("POST", "/contacts", { body: a, project })
|
|
35555
35566
|
})
|
|
35556
35567
|
];
|
|
35557
35568
|
|
|
@@ -35754,7 +35765,7 @@ var generalTools = [
|
|
|
35754
35765
|
];
|
|
35755
35766
|
|
|
35756
35767
|
// src/server.ts
|
|
35757
|
-
var VERSION = "0.
|
|
35768
|
+
var VERSION = "0.4.0";
|
|
35758
35769
|
var INSTRUCTIONS = `Helipod is a cloud platform (Indonesia): containers, managed Postgres databases, VPS, domains, email and an AI gateway, billed from a prepaid wallet in Rupiah.
|
|
35759
35770
|
- Start with list_projects to find projects and environment ids.
|
|
35760
35771
|
- Anything that costs money (create/deploy/scale services, upgrade a database plan, create/renew/upgrade VPS, register/renew/transfer domains): run the dryRun or price lookup first, tell the user the cost in IDR and wait for an explicit yes.
|
package/dist/stdio.js
CHANGED
|
@@ -34866,19 +34866,28 @@ class HelipodClient {
|
|
|
34866
34866
|
return this.request(this.apiUrl, opts.auth === false ? undefined : this.cfg.apiKey, method, path, opts);
|
|
34867
34867
|
}
|
|
34868
34868
|
email(method, path, opts = {}) {
|
|
34869
|
-
|
|
34870
|
-
|
|
34869
|
+
const key = this.cfg.emailApiKey || this.cfg.apiKey;
|
|
34870
|
+
if (!key) {
|
|
34871
|
+
throw new HelipodError(401, "NO_API_KEY", "No key set. Use HELIPOD_API_KEY with the Email API enabled (or HELIPOD_EMAIL_API_KEY). Create keys in the Helipod dashboard: Project → Settings → API keys.");
|
|
34871
34872
|
}
|
|
34872
|
-
|
|
34873
|
+
const headers = !this.cfg.emailApiKey && opts.project ? { "x-helipod-project": opts.project } : undefined;
|
|
34874
|
+
return this.request(this.emailApiUrl, key, method, path, { ...opts, headers });
|
|
34873
34875
|
}
|
|
34874
34876
|
async request(base, key, method, path, opts) {
|
|
34875
|
-
const
|
|
34877
|
+
const raw = path.startsWith("/") ? path : `/${path}`;
|
|
34878
|
+
if (/(^|\/)\.\.?(\/|$)|[?#\\]|\/\/|%2e|%2f|%5c/i.test(raw)) {
|
|
34879
|
+
throw new HelipodError(400, "INVALID_PATH", "An id in this request contains characters that are not allowed.");
|
|
34880
|
+
}
|
|
34881
|
+
const url2 = new URL(base + raw);
|
|
34876
34882
|
for (const [k, v] of Object.entries(opts.query ?? {}))
|
|
34877
34883
|
if (v !== undefined && v !== null && v !== "")
|
|
34878
34884
|
url2.searchParams.set(k, String(v));
|
|
34879
34885
|
const headers = { accept: "application/json", "user-agent": this.cfg.userAgent ?? "helipod-mcp" };
|
|
34880
34886
|
if (key)
|
|
34881
34887
|
headers.authorization = `Bearer ${key}`;
|
|
34888
|
+
for (const [k, v] of Object.entries(opts.headers ?? {}))
|
|
34889
|
+
if (/^[0-9a-f-]{36}$/i.test(v))
|
|
34890
|
+
headers[k] = v;
|
|
34882
34891
|
if (opts.body !== undefined)
|
|
34883
34892
|
headers["content-type"] = "application/json";
|
|
34884
34893
|
if (opts.idempotencyKey)
|
|
@@ -35574,14 +35583,15 @@ var domainTools = [
|
|
|
35574
35583
|
];
|
|
35575
35584
|
|
|
35576
35585
|
// src/tools/email.ts
|
|
35586
|
+
var projectId = exports_external.string().uuid().optional().describe("Project whose email to use. Only needed with a whole-account key that reaches several projects with email.");
|
|
35577
35587
|
var emailTools = [
|
|
35578
35588
|
defineTool({
|
|
35579
35589
|
name: "email_status",
|
|
35580
35590
|
title: "Email status",
|
|
35581
35591
|
kind: "read",
|
|
35582
35592
|
description: "Email service of the key's project: plan, usage today and this month, limits, counts.",
|
|
35583
|
-
input: {},
|
|
35584
|
-
run: (
|
|
35593
|
+
input: { projectId },
|
|
35594
|
+
run: (a, api2) => api2.email("GET", "/status", { project: a.projectId })
|
|
35585
35595
|
}),
|
|
35586
35596
|
defineTool({
|
|
35587
35597
|
name: "send_email",
|
|
@@ -35589,6 +35599,7 @@ var emailTools = [
|
|
|
35589
35599
|
kind: "write",
|
|
35590
35600
|
description: "Send one email from a verified domain of the project (counts against the plan's quota). Use templateId + templateData, or subject + html/text. Pass an idempotencyKey when retrying.",
|
|
35591
35601
|
input: {
|
|
35602
|
+
projectId,
|
|
35592
35603
|
from: exports_external.string().describe('"hello@example.com" or "Acme <hello@example.com>"'),
|
|
35593
35604
|
to: exports_external.array(exports_external.string()).min(1).max(50),
|
|
35594
35605
|
cc: exports_external.array(exports_external.string()).max(50).optional(),
|
|
@@ -35602,63 +35613,63 @@ var emailTools = [
|
|
|
35602
35613
|
variables: exports_external.record(exports_external.string(), exports_external.string()).optional(),
|
|
35603
35614
|
idempotencyKey: exports_external.string().max(128).optional()
|
|
35604
35615
|
},
|
|
35605
|
-
run: (a, api2) => api2.email("POST", "/send", { body: a })
|
|
35616
|
+
run: ({ projectId: project, ...a }, api2) => api2.email("POST", "/send", { body: a, project })
|
|
35606
35617
|
}),
|
|
35607
35618
|
defineTool({
|
|
35608
35619
|
name: "list_emails",
|
|
35609
35620
|
title: "List sent emails",
|
|
35610
35621
|
kind: "read",
|
|
35611
35622
|
description: "Email log, newest first, with delivery status.",
|
|
35612
|
-
input: { status: exports_external.string().optional(), search: exports_external.string().optional(), limit: exports_external.number().int().min(1).max(100).optional(), cursor: exports_external.string().optional() },
|
|
35613
|
-
run: (a, api2) => api2.email("GET", "/emails", { query: a })
|
|
35623
|
+
input: { projectId, status: exports_external.string().optional(), search: exports_external.string().optional(), limit: exports_external.number().int().min(1).max(100).optional(), cursor: exports_external.string().optional() },
|
|
35624
|
+
run: ({ projectId: project, ...a }, api2) => api2.email("GET", "/emails", { query: a, project })
|
|
35614
35625
|
}),
|
|
35615
35626
|
defineTool({
|
|
35616
35627
|
name: "get_email",
|
|
35617
35628
|
title: "Get email",
|
|
35618
35629
|
kind: "read",
|
|
35619
35630
|
description: "One sent email with its status and events.",
|
|
35620
|
-
input: { emailId: exports_external.string() },
|
|
35621
|
-
run: (a, api2) => api2.email("GET", `/emails/${a.emailId}
|
|
35631
|
+
input: { projectId, emailId: exports_external.string() },
|
|
35632
|
+
run: (a, api2) => api2.email("GET", `/emails/${a.emailId}`, { project: a.projectId })
|
|
35622
35633
|
}),
|
|
35623
35634
|
defineTool({
|
|
35624
35635
|
name: "list_email_domains",
|
|
35625
35636
|
title: "List sending domains",
|
|
35626
35637
|
kind: "read",
|
|
35627
35638
|
description: "Sending domains with verification status and the DNS records to add.",
|
|
35628
|
-
input: {},
|
|
35629
|
-
run: (
|
|
35639
|
+
input: { projectId },
|
|
35640
|
+
run: (a, api2) => api2.email("GET", "/domains", { project: a.projectId })
|
|
35630
35641
|
}),
|
|
35631
35642
|
defineTool({
|
|
35632
35643
|
name: "add_email_domain",
|
|
35633
35644
|
title: "Add sending domain",
|
|
35634
35645
|
kind: "write",
|
|
35635
35646
|
description: "Add a sending domain; the response lists the DNS records the user must create, then call verify_email_domain.",
|
|
35636
|
-
input: { domain: exports_external.string() },
|
|
35637
|
-
run: (a, api2) => api2.email("POST", "/domains", { body: { domain: a.domain } })
|
|
35647
|
+
input: { projectId, domain: exports_external.string() },
|
|
35648
|
+
run: (a, api2) => api2.email("POST", "/domains", { body: { domain: a.domain }, project: a.projectId })
|
|
35638
35649
|
}),
|
|
35639
35650
|
defineTool({
|
|
35640
35651
|
name: "verify_email_domain",
|
|
35641
35652
|
title: "Verify sending domain",
|
|
35642
35653
|
kind: "write",
|
|
35643
35654
|
description: "Check a sending domain's DNS records again.",
|
|
35644
|
-
input: { domainId: exports_external.string() },
|
|
35645
|
-
run: (a, api2) => api2.email("POST", `/domains/${a.domainId}/verify
|
|
35655
|
+
input: { projectId, domainId: exports_external.string() },
|
|
35656
|
+
run: (a, api2) => api2.email("POST", `/domains/${a.domainId}/verify`, { project: a.projectId })
|
|
35646
35657
|
}),
|
|
35647
35658
|
defineTool({
|
|
35648
35659
|
name: "list_email_templates",
|
|
35649
35660
|
title: "List email templates",
|
|
35650
35661
|
kind: "read",
|
|
35651
35662
|
description: "Email templates of the project.",
|
|
35652
|
-
input: {},
|
|
35653
|
-
run: (
|
|
35663
|
+
input: { projectId },
|
|
35664
|
+
run: (a, api2) => api2.email("GET", "/templates", { project: a.projectId })
|
|
35654
35665
|
}),
|
|
35655
35666
|
defineTool({
|
|
35656
35667
|
name: "add_contact",
|
|
35657
35668
|
title: "Add contact",
|
|
35658
35669
|
kind: "write",
|
|
35659
35670
|
description: "Add or update an audience contact.",
|
|
35660
|
-
input: { email: exports_external.string(), firstName: exports_external.string().optional(), lastName: exports_external.string().optional() },
|
|
35661
|
-
run: (a, api2) => api2.email("POST", "/contacts", { body: a })
|
|
35671
|
+
input: { projectId, email: exports_external.string(), firstName: exports_external.string().optional(), lastName: exports_external.string().optional() },
|
|
35672
|
+
run: ({ projectId: project, ...a }, api2) => api2.email("POST", "/contacts", { body: a, project })
|
|
35662
35673
|
})
|
|
35663
35674
|
];
|
|
35664
35675
|
|
|
@@ -35861,7 +35872,7 @@ var generalTools = [
|
|
|
35861
35872
|
];
|
|
35862
35873
|
|
|
35863
35874
|
// src/server.ts
|
|
35864
|
-
var VERSION = "0.
|
|
35875
|
+
var VERSION = "0.4.0";
|
|
35865
35876
|
var INSTRUCTIONS = `Helipod is a cloud platform (Indonesia): containers, managed Postgres databases, VPS, domains, email and an AI gateway, billed from a prepaid wallet in Rupiah.
|
|
35866
35877
|
- Start with list_projects to find projects and environment ids.
|
|
35867
35878
|
- Anything that costs money (create/deploy/scale services, upgrade a database plan, create/renew/upgrade VPS, register/renew/transfer domains): run the dryRun or price lookup first, tell the user the cost in IDR and wait for an explicit yes.
|
|
@@ -35891,6 +35902,6 @@ var server = createHelipodServer({
|
|
|
35891
35902
|
emailApiUrl: process.env.HELIPOD_EMAIL_API_URL
|
|
35892
35903
|
});
|
|
35893
35904
|
if (!process.env.HELIPOD_API_KEY && !process.env.HELIPOD_EMAIL_API_KEY) {
|
|
35894
|
-
console.error("helipod-mcp: set HELIPOD_API_KEY (
|
|
35905
|
+
console.error("helipod-mcp: set HELIPOD_API_KEY (enable the Email API on it for the email tools). Create keys in the Helipod dashboard: Project → Settings → API keys.");
|
|
35895
35906
|
}
|
|
35896
35907
|
await server.connect(new StdioServerTransport);
|
package/package.json
CHANGED
package/skills/helipod/SKILL.md
CHANGED
|
@@ -12,7 +12,7 @@ Helipod is a cloud platform in Indonesia: containers (deploy apps from Git, imag
|
|
|
12
12
|
- **MCP tools** (preferred): if tools like `list_projects`, `create_service`, `list_vps_plans` are available, use them.
|
|
13
13
|
- **Without MCP**: call the HTTP API with `Authorization: Bearer $HELIPOD_API_KEY`.
|
|
14
14
|
- Container, Database, VPS, Domain APIs: `https://api.helipod.io/api` (`/containers/...`, `/databases/...`, `/vps/...`, `/domains/...`)
|
|
15
|
-
- Email API: `https://mail.helipod.io/api/email` with `$
|
|
15
|
+
- Email API: `https://mail.helipod.io/api/email` with `$HELIPOD_API_KEY` when the key has the Email API enabled (a whole-account key adds `X-Helipod-Project: <projectId>`), or a separate email key `$HELIPOD_EMAIL_API_KEY`
|
|
16
16
|
- AI gateway: `https://ai.helipod.io/v1` with a `hel_ai_...` key (OpenAI and Anthropic compatible)
|
|
17
17
|
- Keys are created in the dashboard: Project → Settings → API keys. A key is limited to the products and project chosen there; `403 SCOPE_NOT_ALLOWED` means the key lacks that product, `404` on an id means it is outside the key's reach.
|
|
18
18
|
- Every response is `{ "success": true, "data": ... }` or `{ "success": false, "error": "...", "code": "..." }`.
|
|
@@ -5,7 +5,7 @@ description: Send transactional email from an application through Helipod Email
|
|
|
5
5
|
|
|
6
6
|
# Helipod Email
|
|
7
7
|
|
|
8
|
-
Uses
|
|
8
|
+
Uses the Helipod API key `HELIPOD_API_KEY` with **Email API** enabled (Settings → API keys; a whole-account key adds `X-Helipod-Project: <projectId>`), or a separate email key from the Email section as `HELIPOD_EMAIL_API_KEY`. Base URL: `https://mail.helipod.io/api/email`.
|
|
9
9
|
|
|
10
10
|
## Set up
|
|
11
11
|
|
|
@@ -20,7 +20,7 @@ Never put the key in client-side code. Store it as a secret (on Helipod: `set_va
|
|
|
20
20
|
```js
|
|
21
21
|
await fetch("https://mail.helipod.io/api/email/send", {
|
|
22
22
|
method: "POST",
|
|
23
|
-
headers: { Authorization: `Bearer ${process.env.HELIPOD_EMAIL_API_KEY}`, "Content-Type": "application/json" },
|
|
23
|
+
headers: { Authorization: `Bearer ${process.env.HELIPOD_EMAIL_API_KEY ?? process.env.HELIPOD_API_KEY}`, "Content-Type": "application/json" },
|
|
24
24
|
body: JSON.stringify({
|
|
25
25
|
from: "Acme <hello@mail.example.com>",
|
|
26
26
|
to: ["user@example.com"],
|