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 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
- Optional: an **email API key** from the project's **Email** section, for the email tools.
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` (and optionally `HELIPOD_EMAIL_API_KEY`) in its environment.
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_...` (and `X-Helipod-Email-Key` for the email tools). Same tools; the key is used per request and never stored.
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` | (required for email tools) |
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
- if (!this.cfg.emailApiKey) {
35618
- throw new HelipodError(401, "NO_EMAIL_API_KEY", "HELIPOD_EMAIL_API_KEY is not set. Create an email API key in the Helipod dashboard Email section.");
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
- return this.request(this.emailApiUrl, this.cfg.emailApiKey, method, path, opts);
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 url2 = new URL(base + (path.startsWith("/") ? path : `/${path}`));
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: (_a3, api2) => api2.email("GET", "/status")
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: (_a3, api2) => api2.email("GET", "/domains")
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: (_a3, api2) => api2.email("GET", "/templates")
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.3.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
- if (!this.cfg.emailApiKey) {
34763
- throw new HelipodError(401, "NO_EMAIL_API_KEY", "HELIPOD_EMAIL_API_KEY is not set. Create an email API key in the Helipod dashboard Email section.");
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
- return this.request(this.emailApiUrl, this.cfg.emailApiKey, method, path, opts);
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 url2 = new URL(base + (path.startsWith("/") ? path : `/${path}`));
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: (_a3, api2) => api2.email("GET", "/status")
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: (_a3, api2) => api2.email("GET", "/domains")
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: (_a3, api2) => api2.email("GET", "/templates")
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.3.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
- if (!this.cfg.emailApiKey) {
34870
- throw new HelipodError(401, "NO_EMAIL_API_KEY", "HELIPOD_EMAIL_API_KEY is not set. Create an email API key in the Helipod dashboard Email section.");
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
- return this.request(this.emailApiUrl, this.cfg.emailApiKey, method, path, opts);
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 url2 = new URL(base + (path.startsWith("/") ? path : `/${path}`));
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: (_a3, api2) => api2.email("GET", "/status")
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: (_a3, api2) => api2.email("GET", "/domains")
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: (_a3, api2) => api2.email("GET", "/templates")
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.3.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 (and optionally HELIPOD_EMAIL_API_KEY). Create keys in the Helipod dashboard.");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helipod-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "MCP server for Helipod: containers, databases, VPS, domains and email from Claude Code, Codex, Cursor and other AI tools.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 `$HELIPOD_EMAIL_API_KEY` (a separate email key)
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 a separate **email API key** (dashboard → Email → API keys) as `HELIPOD_EMAIL_API_KEY`. Base URL: `https://mail.helipod.io/api/email`.
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"],