stormgtm-mcp 0.1.0 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/index.js +39 -21
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -37,7 +37,9 @@ Then call `whoami`. It returns the account email and credit balance.
37
37
 
38
38
  ## Authentication
39
39
 
40
- The server reads `STORMGTM_API_KEY` first, then `~/.stormgtm/config.json` (the same file `stormgtm login` writes). `STORMGTM_API_URL` overrides the API URL (default `https://stormgtm.com`).
40
+ The server reads `STORMGTM_API_KEY` first, then `~/.stormgtm/config.json` (the same file `stormgtm login` writes), on every tool call. `STORMGTM_API_URL` overrides the API URL (default `https://stormgtm.com`).
41
+
42
+ The server starts without a key. Until you sign in, every tool returns a short message saying how to; after `stormgtm login` the next call works without restarting the server.
41
43
 
42
44
  ```bash
43
45
  npm i -g stormgtm
package/dist/index.js CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
- import { clientFromEnv } from "stormgtm";
5
+ import { clientFromEnv, resolveApiKey } from "stormgtm";
6
6
 
7
7
  // src/server.ts
8
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
- import { StormGTMError, summarize } from "stormgtm";
9
+ import { describeError, MissingApiKeyError, StormGTMError, summarize } from "stormgtm";
10
10
  import { z } from "zod";
11
11
 
12
12
  // src/version.ts
@@ -59,11 +59,26 @@ var policyShape = z.object({
59
59
  function ok(summary, data) {
60
60
  return { content: [{ type: "text", text: summary }], structuredContent: data };
61
61
  }
62
- function fail(error) {
63
- const message = error instanceof StormGTMError ? `${error.code}: ${error.message}` : error instanceof Error ? error.message : String(error);
64
- return { content: [{ type: "text", text: message }], isError: true };
62
+ var NO_KEY_MESSAGE = "StormGTM is not signed in. Run `npx stormgtm login` (or `stormgtm login` if installed globally), or set STORMGTM_API_KEY in this MCP server's environment, then call the tool again.";
63
+ function failureMessage(error, apiUrl) {
64
+ if (error instanceof MissingApiKeyError) return NO_KEY_MESSAGE;
65
+ if (error instanceof StormGTMError) {
66
+ const detail = error.status === 401 || error.status === 402 || error.status === 429 ? describeError(error, apiUrl).message : error.message;
67
+ return `StormGTM request failed (${error.code}): ${detail}`;
68
+ }
69
+ return describeError(error, apiUrl).message;
65
70
  }
66
- function createServer(client) {
71
+ function createServer(source) {
72
+ const api = typeof source === "function" ? source : () => source;
73
+ const fail = (error) => {
74
+ let apiUrl;
75
+ try {
76
+ apiUrl = api().baseUrl;
77
+ } catch {
78
+ apiUrl = void 0;
79
+ }
80
+ return { content: [{ type: "text", text: failureMessage(error, apiUrl) }], isError: true };
81
+ };
67
82
  const server2 = new McpServer({ name: "stormgtm", version: MCP_VERSION }, { instructions: INSTRUCTIONS });
68
83
  server2.registerTool(
69
84
  "check_lead",
@@ -79,7 +94,7 @@ function createServer(client) {
79
94
  },
80
95
  async ({ email, context, tier, policy }) => {
81
96
  try {
82
- const result = await client.check({ email, context, tier, policy });
97
+ const result = await api().check({ email, context, tier, policy });
83
98
  return ok(summarize(result), result);
84
99
  } catch (error) {
85
100
  return fail(error);
@@ -99,7 +114,7 @@ function createServer(client) {
99
114
  },
100
115
  async ({ leads, tier, policy }) => {
101
116
  try {
102
- const created = await client.createBatch({ leads, tier, policy });
117
+ const created = await api().createBatch({ leads, tier, policy });
103
118
  return ok(`Batch ${created.id} queued with ${created.total} leads (up to ${created.maxCredits} credits). Call batch_status with this id.`, created);
104
119
  } catch (error) {
105
120
  return fail(error);
@@ -119,7 +134,7 @@ function createServer(client) {
119
134
  },
120
135
  async ({ id, offset, limit }) => {
121
136
  try {
122
- const status = await client.batch(id, { offset, limit: limit ?? 200 });
137
+ const status = await api().batch(id, { offset, limit: limit ?? 200 });
123
138
  const counts = {};
124
139
  for (const row of status.results) {
125
140
  const key = row.result?.verdict ?? row.status;
@@ -146,7 +161,7 @@ function createServer(client) {
146
161
  },
147
162
  async ({ email, kind, detail }) => {
148
163
  try {
149
- const result = await client.reportOutcome({ email, kind, detail });
164
+ const result = await api().reportOutcome({ email, kind, detail });
150
165
  return ok(result.recorded ? `Recorded ${kind} for ${email}.` : `Not recorded: ${result.rejected.map((r) => r.reason).join(", ")}`, result);
151
166
  } catch (error) {
152
167
  return fail(error);
@@ -162,6 +177,7 @@ function createServer(client) {
162
177
  },
163
178
  async () => {
164
179
  try {
180
+ const client = api();
165
181
  const me = await client.me();
166
182
  return ok(`${me.email}: ${me.credits} credits (${client.baseUrl})`, { id: me.id, email: me.email, credits: me.credits, apiUrl: client.baseUrl });
167
183
  } catch (error) {
@@ -178,7 +194,7 @@ function createServer(client) {
178
194
  },
179
195
  async () => {
180
196
  try {
181
- const me = await client.me();
197
+ const me = await api().me();
182
198
  return ok(`${me.credits} credits. fast=${me.pricing.fast}, deep=${me.pricing.deep}, unknown=free. 30d: ${me.usage30d.total} checks.`, me);
183
199
  } catch (error) {
184
200
  return fail(error);
@@ -206,7 +222,7 @@ function createServer(client) {
206
222
  },
207
223
  async ({ messages }) => {
208
224
  try {
209
- const result = await client.send(messages);
225
+ const result = await api().send(messages);
210
226
  const lines = [`${result.accepted.length} queued, ${result.rejected.length} rejected.`];
211
227
  for (const entry of result.accepted) lines.push(`queued ${entry.id} \u2192 ${entry.to}${entry.duplicate ? " (already queued)" : ""}`);
212
228
  for (const entry of result.rejected) lines.push(`rejected #${entry.index}: ${entry.code} \u2014 ${entry.message}`);
@@ -225,7 +241,7 @@ function createServer(client) {
225
241
  },
226
242
  async () => {
227
243
  try {
228
- const domains = await client.domains();
244
+ const domains = await api().domains();
229
245
  const lines = domains.length ? domains.map((domain) => `${domain.name} (${domain.id}): ${domain.status}, ${domain.warmup.paused ? "paused" : `${domain.warmup.dailyCap}/day`}`) : ["No sending domains yet. Add one in the StormGTM dashboard."];
230
246
  return ok(lines.join("\n"), { domains });
231
247
  } catch (error) {
@@ -242,7 +258,7 @@ function createServer(client) {
242
258
  },
243
259
  async ({ id }) => {
244
260
  try {
245
- const health = await client.domainHealth(id);
261
+ const health = await api().domainHealth(id);
246
262
  const pct = (value) => `${(value * 100).toFixed(1)}%`;
247
263
  const state = health.warmup.paused ? `paused (${health.warmup.pausedReason ?? "manual"})` : `${health.warmup.remainingToday}/${health.warmup.dailyCap} left today`;
248
264
  return ok(`${health.name}: ${state}. 7d: ${health.last7Days.sent} sent, ${pct(health.last7Days.bounceRate)} bounced, ${pct(health.last7Days.complaintRate)} complaints.`, health);
@@ -260,7 +276,7 @@ function createServer(client) {
260
276
  },
261
277
  async ({ id }) => {
262
278
  try {
263
- const email = await client.email(id);
279
+ const email = await api().email(id);
264
280
  return ok(`${email.id} \u2192 ${email.to}: ${email.status}, delivery ${email.delivery}${email.error ? ` (${email.error})` : ""}`, email);
265
281
  } catch (error) {
266
282
  return fail(error);
@@ -281,7 +297,7 @@ function createServer(client) {
281
297
  },
282
298
  async ({ name, from, replyTo, steps }) => {
283
299
  try {
284
- const sequence = await client.createSequence({ name, from, replyTo, steps });
300
+ const sequence = await api().createSequence({ name, from, replyTo, steps });
285
301
  const variables = sequence.variables.length ? ` Leads need: ${sequence.variables.join(", ")}.` : "";
286
302
  return ok(`Created sequence ${sequence.id} "${sequence.name}" with ${sequence.steps.length} steps.${variables} Enroll leads with enroll_leads.`, sequence);
287
303
  } catch (error) {
@@ -301,7 +317,7 @@ function createServer(client) {
301
317
  },
302
318
  async ({ sequenceId, leads }) => {
303
319
  try {
304
- const result = await client.enroll(sequenceId, leads);
320
+ const result = await api().enroll(sequenceId, leads);
305
321
  const fresh = result.accepted.filter((entry) => !entry.duplicate).length;
306
322
  const lines = [`${fresh} enrolled, ${result.accepted.length - fresh} already enrolled, ${result.rejected.length} rejected. Up to ${result.maxCredits} credits.`];
307
323
  for (const entry of result.rejected.slice(0, 50)) lines.push(`rejected #${entry.index}: ${entry.code} \u2014 ${entry.message}`);
@@ -321,11 +337,11 @@ function createServer(client) {
321
337
  async ({ id }) => {
322
338
  try {
323
339
  if (!id) {
324
- const sequences = await client.sequences();
340
+ const sequences = await api().sequences();
325
341
  const lines2 = sequences.length ? sequences.map((entry) => `${entry.id} "${entry.name}"${entry.archivedAt ? " (archived)" : ""}: ${entry.steps} steps, ${entry.counts.active} active, ${entry.counts.completed} completed, ${entry.counts.stopped} stopped`) : ["No sequences yet. Create one with create_sequence."];
326
342
  return ok(lines2.join("\n"), { sequences });
327
343
  }
328
- const [sequence, enrollments] = await Promise.all([client.sequence(id), client.enrollments(id, 100)]);
344
+ const [sequence, enrollments] = await Promise.all([api().sequence(id), api().enrollments(id, 100)]);
329
345
  const lines = [`${sequence.id} "${sequence.name}" from ${sequence.from}: ${sequence.steps.length} steps`];
330
346
  for (const entry of enrollments.slice(0, 50)) lines.push(`${entry.email}: ${entry.status}${entry.stopReason ? ` (${entry.stopReason})` : entry.nextStep !== null ? `, step ${entry.nextStep + 1} next` : ""}`);
331
347
  return ok(lines.join("\n"), { sequence, enrollments });
@@ -343,7 +359,7 @@ function createServer(client) {
343
359
  },
344
360
  async ({ sequenceId, enrollmentId }) => {
345
361
  try {
346
- const result = await client.stopEnrollment(sequenceId, enrollmentId);
362
+ const result = await api().stopEnrollment(sequenceId, enrollmentId);
347
363
  return ok(`Stopped ${result.id}.`, result);
348
364
  } catch (error) {
349
365
  return fail(error);
@@ -354,5 +370,7 @@ function createServer(client) {
354
370
  }
355
371
 
356
372
  // src/index.ts
357
- var server = createServer(clientFromEnv());
373
+ if (!resolveApiKey()) process.stderr.write(`stormgtm-mcp: ${NO_KEY_MESSAGE}
374
+ `);
375
+ var server = createServer(() => clientFromEnv());
358
376
  await server.connect(new StdioServerTransport());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stormgtm-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "StormGTM MCP server — qualify leads with Barometer and send through your own domains, over stdio",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,7 +13,7 @@
13
13
  "lead-qualification"
14
14
  ],
15
15
  "bin": {
16
- "stormgtm-mcp": "./dist/index.js"
16
+ "stormgtm-mcp": "dist/index.js"
17
17
  },
18
18
  "homepage": "https://github.com/marginsystems/stormgtm-mcp",
19
19
  "repository": {
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@modelcontextprotocol/sdk": "^1.31.0",
42
- "stormgtm": "0.1.0",
42
+ "stormgtm": "0.1.1",
43
43
  "zod": "^4.6.5"
44
44
  },
45
45
  "devDependencies": {