engagelab-sms-mcp 1.0.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/LICENSE +21 -0
- package/README.md +129 -0
- package/build/config.js +35 -0
- package/build/engagelab/client.js +199 -0
- package/build/index.js +21 -0
- package/build/test-client.js +77 -0
- package/build/tools/send-sms.js +98 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DevEngageLab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# engagelab-sms-mcp
|
|
2
|
+
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io/) server that lets AI assistants send SMS messages through [EngageLab](https://www.engagelab.com/).
|
|
4
|
+
|
|
5
|
+
Add it to your MCP client (Cursor, Claude Desktop, etc.) and the AI can send template-based SMS on your behalf.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- Node.js 18+
|
|
10
|
+
- An EngageLab account with SMS API credentials (`dev_key` and `dev_secret`)
|
|
11
|
+
- At least one approved SMS template in your EngageLab console
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### Cursor
|
|
16
|
+
|
|
17
|
+
Go to **Settings > MCP**, click **Add new MCP server**, and paste:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"engagelab-sms": {
|
|
23
|
+
"command": "npx",
|
|
24
|
+
"args": ["-y", "engagelab-sms-mcp"],
|
|
25
|
+
"env": {
|
|
26
|
+
"ENGAGELAB_DEV_KEY": "<your_dev_key>",
|
|
27
|
+
"ENGAGELAB_DEV_SECRET": "<your_dev_secret>"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Claude Desktop
|
|
35
|
+
|
|
36
|
+
Open **Settings > Developer > Edit Config** and add to `mcpServers`:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"engagelab-sms": {
|
|
42
|
+
"command": "npx",
|
|
43
|
+
"args": ["-y", "engagelab-sms-mcp"],
|
|
44
|
+
"env": {
|
|
45
|
+
"ENGAGELAB_DEV_KEY": "<your_dev_key>",
|
|
46
|
+
"ENGAGELAB_DEV_SECRET": "<your_dev_secret>"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Other MCP Clients
|
|
54
|
+
|
|
55
|
+
Any MCP client that supports `stdio` transport can use this server. Point the command to `npx -y engagelab-sms-mcp` and pass the two required environment variables.
|
|
56
|
+
|
|
57
|
+
## Available Tools
|
|
58
|
+
|
|
59
|
+
### `send_sms`
|
|
60
|
+
|
|
61
|
+
Send SMS messages through EngageLab using a pre-approved template.
|
|
62
|
+
|
|
63
|
+
**Input:**
|
|
64
|
+
|
|
65
|
+
| Field | Type | Required | Description |
|
|
66
|
+
|-------|------|----------|-------------|
|
|
67
|
+
| `to` | `string[]` | Yes | Target phone numbers (international format recommended, e.g. `+8618700001111`) |
|
|
68
|
+
| `template.id` | `string` | Yes | Approved EngageLab SMS template ID |
|
|
69
|
+
| `template.params` | `object` | Yes | Template variable values, e.g. `{"code": "123456"}` |
|
|
70
|
+
|
|
71
|
+
**Example input:**
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"to": ["+8618700001111"],
|
|
76
|
+
"template": {
|
|
77
|
+
"id": "your-template-id",
|
|
78
|
+
"params": {
|
|
79
|
+
"code": "123456"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Output:**
|
|
86
|
+
|
|
87
|
+
| Field | Type | Description |
|
|
88
|
+
|-------|------|-------------|
|
|
89
|
+
| `success` | `boolean` | Whether the request was accepted |
|
|
90
|
+
| `plan_id` | `string` | EngageLab plan ID for tracking |
|
|
91
|
+
| `total_count` | `number` | Total recipients submitted |
|
|
92
|
+
| `accepted_count` | `number` | Recipients accepted for delivery |
|
|
93
|
+
| `message_id` | `string` | Message identifier (if available) |
|
|
94
|
+
| `message` | `string` | Status or error description |
|
|
95
|
+
| `code` | `number` | EngageLab response code (`0` = success) |
|
|
96
|
+
|
|
97
|
+
## Environment Variables
|
|
98
|
+
|
|
99
|
+
| Variable | Required | Default | Description |
|
|
100
|
+
|----------|----------|---------|-------------|
|
|
101
|
+
| `ENGAGELAB_DEV_KEY` | Yes | — | Your EngageLab dev key |
|
|
102
|
+
| `ENGAGELAB_DEV_SECRET` | Yes | — | Your EngageLab dev secret |
|
|
103
|
+
| `ENGAGELAB_BASE_URL` | No | `https://smsapi.engagelab.com` | API base URL |
|
|
104
|
+
| `ENGAGELAB_REQUEST_TIMEOUT_MS` | No | `10000` | Request timeout in milliseconds |
|
|
105
|
+
| `ENGAGELAB_MAX_RETRIES` | No | `1` | Max retry attempts for transient failures |
|
|
106
|
+
|
|
107
|
+
## Troubleshooting
|
|
108
|
+
|
|
109
|
+
**Server fails to start with "Missing required environment variable"**
|
|
110
|
+
- Make sure both `ENGAGELAB_DEV_KEY` and `ENGAGELAB_DEV_SECRET` are set in the `env` block of your MCP client config.
|
|
111
|
+
|
|
112
|
+
**`send_sms` returns error code 3002 ("invalid template id format")**
|
|
113
|
+
- Check that your template ID matches an approved template in the EngageLab console.
|
|
114
|
+
|
|
115
|
+
**`send_sms` returns error code related to template params**
|
|
116
|
+
- Verify that `template.params` keys match the variable names defined in your EngageLab template.
|
|
117
|
+
|
|
118
|
+
**SMS not received**
|
|
119
|
+
- Use international phone number format (e.g. `+8618700001111`).
|
|
120
|
+
- Confirm the template is approved and not suspended.
|
|
121
|
+
|
|
122
|
+
## Links
|
|
123
|
+
|
|
124
|
+
- [EngageLab SMS API Documentation](https://www.engagelab.com/docs/NEWSMS/REST-API/API-SMS-Sending)
|
|
125
|
+
- [Model Context Protocol](https://modelcontextprotocol.io/)
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
package/build/config.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const DEFAULT_BASE_URL = "https://smsapi.engagelab.com";
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
3
|
+
const DEFAULT_MAX_RETRIES = 1;
|
|
4
|
+
function getRequiredEnv(name) {
|
|
5
|
+
const value = process.env[name];
|
|
6
|
+
if (!value || value.trim().length === 0) {
|
|
7
|
+
throw new Error(`Missing required environment variable ${name}. ` +
|
|
8
|
+
"Please set ENGAGELAB_DEV_KEY and ENGAGELAB_DEV_SECRET before starting the MCP server.");
|
|
9
|
+
}
|
|
10
|
+
return value.trim();
|
|
11
|
+
}
|
|
12
|
+
function parsePositiveInt(value, defaultValue) {
|
|
13
|
+
if (!value || value.trim().length === 0) {
|
|
14
|
+
return defaultValue;
|
|
15
|
+
}
|
|
16
|
+
const parsed = Number.parseInt(value, 10);
|
|
17
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
18
|
+
throw new Error(`Invalid numeric environment variable value: ${value}`);
|
|
19
|
+
}
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
export function loadConfig() {
|
|
23
|
+
const devKey = getRequiredEnv("ENGAGELAB_DEV_KEY");
|
|
24
|
+
const devSecret = getRequiredEnv("ENGAGELAB_DEV_SECRET");
|
|
25
|
+
const baseUrl = (process.env.ENGAGELAB_BASE_URL ?? DEFAULT_BASE_URL).trim();
|
|
26
|
+
const requestTimeoutMs = parsePositiveInt(process.env.ENGAGELAB_REQUEST_TIMEOUT_MS, DEFAULT_TIMEOUT_MS);
|
|
27
|
+
const maxRetries = parsePositiveInt(process.env.ENGAGELAB_MAX_RETRIES, DEFAULT_MAX_RETRIES);
|
|
28
|
+
return {
|
|
29
|
+
baseUrl,
|
|
30
|
+
devKey,
|
|
31
|
+
devSecret,
|
|
32
|
+
requestTimeoutMs,
|
|
33
|
+
maxRetries,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
export class EngageLabApiError extends Error {
|
|
2
|
+
retriable;
|
|
3
|
+
statusCode;
|
|
4
|
+
apiCode;
|
|
5
|
+
apiMessage;
|
|
6
|
+
apiResponse;
|
|
7
|
+
constructor(args) {
|
|
8
|
+
super(args.message);
|
|
9
|
+
this.name = "EngageLabApiError";
|
|
10
|
+
this.retriable = args.retriable;
|
|
11
|
+
this.statusCode = args.statusCode;
|
|
12
|
+
this.apiCode = args.apiCode;
|
|
13
|
+
this.apiMessage = args.apiMessage;
|
|
14
|
+
this.apiResponse = args.apiResponse;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function buildAuthHeader(devKey, devSecret) {
|
|
18
|
+
return `Basic ${Buffer.from(`${devKey}:${devSecret}`).toString("base64")}`;
|
|
19
|
+
}
|
|
20
|
+
function toErrorMessage(error) {
|
|
21
|
+
if (error instanceof Error && error.message) {
|
|
22
|
+
return error.message;
|
|
23
|
+
}
|
|
24
|
+
return "Unknown error";
|
|
25
|
+
}
|
|
26
|
+
function isRetriableStatus(status) {
|
|
27
|
+
return status === 429 || status >= 500;
|
|
28
|
+
}
|
|
29
|
+
function getRetryBackoffMs(attempt) {
|
|
30
|
+
return 300 * Math.pow(2, attempt);
|
|
31
|
+
}
|
|
32
|
+
async function sleep(ms) {
|
|
33
|
+
await new Promise((resolve) => {
|
|
34
|
+
setTimeout(resolve, ms);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async function fetchWithTimeout(url, init, timeoutMs) {
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
40
|
+
try {
|
|
41
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
45
|
+
throw new EngageLabApiError({
|
|
46
|
+
message: `Request timed out after ${timeoutMs}ms.`,
|
|
47
|
+
retriable: true,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
throw new EngageLabApiError({
|
|
51
|
+
message: `Network request failed: ${toErrorMessage(error)}`,
|
|
52
|
+
retriable: true,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
clearTimeout(timeout);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function tryParseJson(response) {
|
|
60
|
+
const rawBody = await response.text();
|
|
61
|
+
if (!rawBody) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(rawBody);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function readApiMessage(value) {
|
|
72
|
+
if (typeof value.message === "string" && value.message.trim().length > 0) {
|
|
73
|
+
return value.message;
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
function readApiCode(value) {
|
|
78
|
+
if (typeof value.code === "number") {
|
|
79
|
+
return value.code;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
function buildOptionalApiResponse(value) {
|
|
84
|
+
if (typeof value.plan_id === "string" &&
|
|
85
|
+
typeof value.total_count === "number" &&
|
|
86
|
+
typeof value.accepted_count === "number") {
|
|
87
|
+
return {
|
|
88
|
+
plan_id: value.plan_id,
|
|
89
|
+
total_count: value.total_count,
|
|
90
|
+
accepted_count: value.accepted_count,
|
|
91
|
+
message: typeof value.message === "string" ? value.message : undefined,
|
|
92
|
+
code: typeof value.code === "number" ? value.code : undefined,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
function parseSendSmsSuccess(data) {
|
|
98
|
+
if (!data || typeof data !== "object") {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
const raw = data;
|
|
102
|
+
if (typeof raw.plan_id !== "string" ||
|
|
103
|
+
typeof raw.total_count !== "number" ||
|
|
104
|
+
typeof raw.accepted_count !== "number") {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
plan_id: raw.plan_id,
|
|
109
|
+
total_count: raw.total_count,
|
|
110
|
+
accepted_count: raw.accepted_count,
|
|
111
|
+
message_id: typeof raw.message_id === "string" ? raw.message_id : undefined,
|
|
112
|
+
message: typeof raw.message === "string" ? raw.message : undefined,
|
|
113
|
+
code: typeof raw.code === "number" ? raw.code : undefined,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function buildHttpError(statusCode, parsedBody) {
|
|
117
|
+
let apiMessage;
|
|
118
|
+
let apiCode;
|
|
119
|
+
let apiResponse;
|
|
120
|
+
if (parsedBody && typeof parsedBody === "object") {
|
|
121
|
+
const body = parsedBody;
|
|
122
|
+
apiMessage = readApiMessage(body);
|
|
123
|
+
apiCode = readApiCode(body);
|
|
124
|
+
apiResponse = buildOptionalApiResponse(body);
|
|
125
|
+
}
|
|
126
|
+
const baseMessage = `EngageLab API request failed with HTTP ${statusCode}.`;
|
|
127
|
+
const message = apiMessage ? `${baseMessage} ${apiMessage}` : baseMessage;
|
|
128
|
+
return new EngageLabApiError({
|
|
129
|
+
message,
|
|
130
|
+
retriable: isRetriableStatus(statusCode),
|
|
131
|
+
statusCode,
|
|
132
|
+
apiCode,
|
|
133
|
+
apiMessage,
|
|
134
|
+
apiResponse,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function buildApiError(response) {
|
|
138
|
+
const message = response.message && response.message.length > 0
|
|
139
|
+
? `EngageLab API business error: ${response.message}`
|
|
140
|
+
: "EngageLab API business error.";
|
|
141
|
+
return new EngageLabApiError({
|
|
142
|
+
message,
|
|
143
|
+
retriable: false,
|
|
144
|
+
statusCode: 200,
|
|
145
|
+
apiCode: response.code,
|
|
146
|
+
apiMessage: response.message,
|
|
147
|
+
apiResponse: response,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
export async function sendSms(config, payload) {
|
|
151
|
+
const endpoint = new URL("/v1/messages", config.baseUrl).toString();
|
|
152
|
+
const authHeader = buildAuthHeader(config.devKey, config.devSecret);
|
|
153
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt += 1) {
|
|
154
|
+
try {
|
|
155
|
+
const response = await fetchWithTimeout(endpoint, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: {
|
|
158
|
+
"Content-Type": "application/json",
|
|
159
|
+
Authorization: authHeader,
|
|
160
|
+
},
|
|
161
|
+
body: JSON.stringify(payload),
|
|
162
|
+
}, config.requestTimeoutMs);
|
|
163
|
+
const parsedBody = await tryParseJson(response);
|
|
164
|
+
if (!response.ok) {
|
|
165
|
+
throw buildHttpError(response.status, parsedBody);
|
|
166
|
+
}
|
|
167
|
+
const parsedSuccess = parseSendSmsSuccess(parsedBody);
|
|
168
|
+
if (!parsedSuccess) {
|
|
169
|
+
throw new EngageLabApiError({
|
|
170
|
+
message: "EngageLab API returned an unexpected response shape.",
|
|
171
|
+
retriable: false,
|
|
172
|
+
statusCode: response.status,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (typeof parsedSuccess.code === "number" && parsedSuccess.code !== 0) {
|
|
176
|
+
throw buildApiError(parsedSuccess);
|
|
177
|
+
}
|
|
178
|
+
return parsedSuccess;
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
const normalized = error instanceof EngageLabApiError
|
|
182
|
+
? error
|
|
183
|
+
: new EngageLabApiError({
|
|
184
|
+
message: `Unexpected error while calling EngageLab API: ${toErrorMessage(error)}`,
|
|
185
|
+
retriable: false,
|
|
186
|
+
});
|
|
187
|
+
const canRetry = normalized.retriable && attempt < config.maxRetries;
|
|
188
|
+
if (canRetry) {
|
|
189
|
+
await sleep(getRetryBackoffMs(attempt));
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
throw normalized;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
throw new EngageLabApiError({
|
|
196
|
+
message: "Failed to call EngageLab API after retries.",
|
|
197
|
+
retriable: false,
|
|
198
|
+
});
|
|
199
|
+
}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { loadConfig } from "./config.js";
|
|
5
|
+
import { registerSendSmsTool } from "./tools/send-sms.js";
|
|
6
|
+
const server = new McpServer({
|
|
7
|
+
name: "engagelab-sms-mcp",
|
|
8
|
+
version: "1.0.0",
|
|
9
|
+
});
|
|
10
|
+
const config = loadConfig();
|
|
11
|
+
registerSendSmsTool(server, config);
|
|
12
|
+
async function main() {
|
|
13
|
+
const transport = new StdioServerTransport();
|
|
14
|
+
await server.connect(transport);
|
|
15
|
+
console.error("engagelab-sms-mcp is running on stdio");
|
|
16
|
+
}
|
|
17
|
+
main().catch((error) => {
|
|
18
|
+
const message = error instanceof Error ? error.message : "Unknown startup error";
|
|
19
|
+
console.error(`Fatal error in main(): ${message}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
5
|
+
function getServerScriptPath() {
|
|
6
|
+
return path.resolve(process.cwd(), "build", "index.js");
|
|
7
|
+
}
|
|
8
|
+
async function assertBuildExists(serverScriptPath) {
|
|
9
|
+
try {
|
|
10
|
+
await access(serverScriptPath);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
throw new Error(`Cannot find built MCP server at ${serverScriptPath}. Run "npm run build" first.`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function readPayloadFromEnv() {
|
|
17
|
+
const raw = process.env.SMS_TEST_PAYLOAD;
|
|
18
|
+
if (!raw || raw.trim().length === 0) {
|
|
19
|
+
throw new Error("SMS_TEST_PAYLOAD is required for --call mode. " +
|
|
20
|
+
'Example: {"to":["+8618701235678"],"template":{"id":"tpl-id","params":{"code":"123456"}}}');
|
|
21
|
+
}
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
parsed = JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new Error("SMS_TEST_PAYLOAD is not valid JSON.");
|
|
28
|
+
}
|
|
29
|
+
if (!parsed || typeof parsed !== "object") {
|
|
30
|
+
throw new Error("SMS_TEST_PAYLOAD must be a JSON object.");
|
|
31
|
+
}
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
async function main() {
|
|
35
|
+
const serverScriptPath = getServerScriptPath();
|
|
36
|
+
await assertBuildExists(serverScriptPath);
|
|
37
|
+
const shouldCall = process.argv.includes("--call");
|
|
38
|
+
const childEnv = {
|
|
39
|
+
...process.env,
|
|
40
|
+
ENGAGELAB_DEV_KEY: process.env.ENGAGELAB_DEV_KEY ?? "test_key_for_list_mode",
|
|
41
|
+
ENGAGELAB_DEV_SECRET: process.env.ENGAGELAB_DEV_SECRET ?? "test_secret_for_list_mode",
|
|
42
|
+
};
|
|
43
|
+
const transport = new StdioClientTransport({
|
|
44
|
+
command: process.execPath,
|
|
45
|
+
args: [serverScriptPath],
|
|
46
|
+
env: childEnv,
|
|
47
|
+
});
|
|
48
|
+
const client = new Client({
|
|
49
|
+
name: "engagelab-sms-mcp-test-client",
|
|
50
|
+
version: "1.0.0",
|
|
51
|
+
});
|
|
52
|
+
try {
|
|
53
|
+
await client.connect(transport);
|
|
54
|
+
const listResult = await client.listTools();
|
|
55
|
+
const toolNames = listResult.tools.map((tool) => tool.name);
|
|
56
|
+
console.log("Connected. Tools:", toolNames);
|
|
57
|
+
if (!shouldCall) {
|
|
58
|
+
console.log('List-only mode complete. Use "--call" to invoke send_sms.');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const payload = readPayloadFromEnv();
|
|
62
|
+
const callResult = await client.callTool({
|
|
63
|
+
name: "send_sms",
|
|
64
|
+
arguments: payload,
|
|
65
|
+
});
|
|
66
|
+
console.log("send_sms result:");
|
|
67
|
+
console.log(JSON.stringify(callResult, null, 2));
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
await client.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
main().catch((error) => {
|
|
74
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
75
|
+
console.error(`Test client failed: ${message}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { EngageLabApiError, sendSms } from "../engagelab/client.js";
|
|
3
|
+
const sendSmsInputSchema = z.object({
|
|
4
|
+
to: z
|
|
5
|
+
.array(z.string().trim().min(1).describe("Phone number in international format."))
|
|
6
|
+
.min(1)
|
|
7
|
+
.describe("Target phone numbers."),
|
|
8
|
+
template: z.object({
|
|
9
|
+
id: z.string().trim().min(1).describe("Approved EngageLab SMS template ID."),
|
|
10
|
+
params: z
|
|
11
|
+
.record(z.union([z.string(), z.number(), z.boolean()]))
|
|
12
|
+
.describe("Template variable assignments, e.g. {\"name\":\"Bob\"}."),
|
|
13
|
+
}),
|
|
14
|
+
});
|
|
15
|
+
const sendSmsOutputSchema = z.object({
|
|
16
|
+
success: z.boolean(),
|
|
17
|
+
plan_id: z.string().optional(),
|
|
18
|
+
total_count: z.number().optional(),
|
|
19
|
+
accepted_count: z.number().optional(),
|
|
20
|
+
message_id: z.string().optional(),
|
|
21
|
+
message: z.string().optional(),
|
|
22
|
+
code: z.number().optional(),
|
|
23
|
+
});
|
|
24
|
+
function toErrorMessage(error) {
|
|
25
|
+
if (error instanceof Error && error.message) {
|
|
26
|
+
return error.message;
|
|
27
|
+
}
|
|
28
|
+
return "Unknown error";
|
|
29
|
+
}
|
|
30
|
+
export function registerSendSmsTool(server, config) {
|
|
31
|
+
server.registerTool("send_sms", {
|
|
32
|
+
title: "Send SMS",
|
|
33
|
+
description: "Send SMS through EngageLab using a template. Requires ENGAGELAB_DEV_KEY and ENGAGELAB_DEV_SECRET environment variables.",
|
|
34
|
+
inputSchema: sendSmsInputSchema,
|
|
35
|
+
outputSchema: sendSmsOutputSchema,
|
|
36
|
+
annotations: {
|
|
37
|
+
readOnlyHint: false,
|
|
38
|
+
destructiveHint: false,
|
|
39
|
+
idempotentHint: false,
|
|
40
|
+
openWorldHint: true,
|
|
41
|
+
},
|
|
42
|
+
}, async ({ to, template }) => {
|
|
43
|
+
try {
|
|
44
|
+
const response = await sendSms(config, { to, template });
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: "text",
|
|
49
|
+
text: `SMS request accepted. plan_id=${response.plan_id}, accepted_count=${response.accepted_count}/${response.total_count}.`,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
structuredContent: {
|
|
53
|
+
success: true,
|
|
54
|
+
plan_id: response.plan_id,
|
|
55
|
+
total_count: response.total_count,
|
|
56
|
+
accepted_count: response.accepted_count,
|
|
57
|
+
message_id: response.message_id,
|
|
58
|
+
message: response.message,
|
|
59
|
+
code: response.code,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error instanceof EngageLabApiError) {
|
|
65
|
+
return {
|
|
66
|
+
isError: true,
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: "text",
|
|
70
|
+
text: `send_sms failed: ${error.message}`,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
structuredContent: {
|
|
74
|
+
success: false,
|
|
75
|
+
plan_id: error.apiResponse?.plan_id,
|
|
76
|
+
total_count: error.apiResponse?.total_count,
|
|
77
|
+
accepted_count: error.apiResponse?.accepted_count,
|
|
78
|
+
message: error.apiMessage,
|
|
79
|
+
code: error.apiCode,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
isError: true,
|
|
85
|
+
content: [
|
|
86
|
+
{
|
|
87
|
+
type: "text",
|
|
88
|
+
text: `send_sms failed with an unexpected error: ${toErrorMessage(error)}`,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
structuredContent: {
|
|
92
|
+
success: false,
|
|
93
|
+
message: toErrorMessage(error),
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "engagelab-sms-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for sending SMS through EngageLab",
|
|
5
|
+
"main": "./build/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"engagelab-sms-mcp": "./build/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"prepublishOnly": "npm run build",
|
|
13
|
+
"test:mcp:list": "npm run build && node build/test-client.js",
|
|
14
|
+
"test:mcp:call": "npm run build && node build/test-client.js --call"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"build"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"sms",
|
|
22
|
+
"engagelab",
|
|
23
|
+
"model-context-protocol"
|
|
24
|
+
],
|
|
25
|
+
"author": "DevEngageLab",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/DevEngageLab/engagelab-sms-mcp.git"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/DevEngageLab/engagelab-sms-mcp",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
37
|
+
"zod": "^3.25.76"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^25.5.0",
|
|
41
|
+
"typescript": "^5.9.3"
|
|
42
|
+
}
|
|
43
|
+
}
|