@hasna/skills 0.1.32 → 0.1.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -16
- package/bin/index.js +57 -14
- package/bin/mcp.js +57 -13
- package/dist/cli/commands/runtime.d.ts +1 -1
- package/dist/index.js +51 -5
- package/dist/lib/skill-validation.d.ts +2 -0
- package/package.json +1 -1
- package/skills/ad-creative-pack/package.json +4 -7
- package/skills/api-docs-portal/package.json +5 -3
- package/skills/audio/package.json +4 -22
- package/skills/audio-transcript-pack/package.json +3 -2
- package/skills/blog-article/package.json +5 -21
- package/skills/brand-kit/package.json +4 -7
- package/skills/brand-photo-shoot/package.json +3 -2
- package/skills/browse/README.md +13 -256
- package/skills/browse/SKILL.md +24 -45
- package/skills/browse/package.json +4 -33
- package/skills/contract-review-report/package.json +3 -2
- package/skills/customer-feedback-report/package.json +3 -2
- package/skills/deepresearch/CLAUDE.md +6 -39
- package/skills/deepresearch/README.md +15 -150
- package/skills/deepresearch/SKILL.md +23 -32
- package/skills/deepresearch/package.json +4 -37
- package/skills/domainpurchase/SKILL.md +14 -31
- package/skills/domainpurchase/package.json +3 -8
- package/skills/email-sequence/package.json +4 -7
- package/skills/generate-book-cover/package.json +5 -23
- package/skills/icon-pack/package.json +4 -22
- package/skills/image/README.md +13 -183
- package/skills/image/package.json +4 -22
- package/skills/invoice-reconciliation/package.json +3 -2
- package/skills/landing-page-pack/package.json +4 -7
- package/skills/logo-design/package.json +4 -2
- package/skills/managemcp/CLAUDE.md +9 -43
- package/skills/managemcp/README.md +12 -198
- package/skills/managemcp/package.json +2 -10
- package/skills/managemcp/tsconfig.json +1 -3
- package/skills/market-research-report/package.json +5 -4
- package/skills/meeting-pack/package.json +3 -2
- package/skills/migration-plan-pack/package.json +3 -2
- package/skills/music/package.json +4 -20
- package/skills/music-album/package.json +3 -2
- package/skills/one-page-website/package.json +3 -2
- package/skills/pdf-read/package.json +5 -23
- package/skills/pdf-to-dataset/package.json +5 -4
- package/skills/pdf-to-markdown/package.json +5 -4
- package/skills/performance-audit-report/package.json +3 -2
- package/skills/photo-album/package.json +4 -2
- package/skills/pitch-deck/package.json +5 -3
- package/skills/playlist-maker/package.json +4 -2
- package/skills/product-mockup/package.json +3 -2
- package/skills/proposal-pack/package.json +5 -4
- package/skills/read-pdf/package.json +4 -29
- package/skills/remove-background/package.json +5 -21
- package/skills/repo-onboarding-report/package.json +3 -2
- package/skills/sdk-generator/package.json +3 -2
- package/skills/security-audit-report/package.json +5 -4
- package/skills/seo-content-pack/package.json +5 -3
- package/skills/short-video-pack/package.json +3 -2
- package/skills/slide-deck-generator/package.json +3 -2
- package/skills/sms/README.md +18 -56
- package/skills/sms/package.json +1 -3
- package/skills/sms/tsconfig.json +1 -2
- package/skills/social-content-calendar/package.json +4 -7
- package/skills/test-suite-generator/package.json +5 -4
- package/skills/transcript/package.json +4 -23
- package/skills/video/package.json +4 -24
- package/skills/video-highlight-pack/package.json +3 -2
- package/skills/voiceover-jingle-pack/package.json +3 -2
- package/skills/webcrawling/CLAUDE.md +6 -17
- package/skills/webcrawling/README.md +13 -66
- package/skills/webcrawling/package.json +4 -32
- package/skills/browse/.env.example +0 -14
- package/skills/deepresearch/.env.example +0 -9
- package/skills/domainpurchase/.env.example +0 -7
- package/skills/domainpurchase/src/lib/api-client.ts +0 -571
- package/skills/domainpurchase/src/lib/config.ts +0 -209
- package/skills/domainpurchase/src/lib/installer.ts +0 -165
- package/skills/image/.env.example +0 -12
- package/skills/image/PROJECT_OVERVIEW.md +0 -216
- package/skills/image/QUICKSTART.md +0 -111
- package/skills/managemcp/.env.example +0 -16
- package/skills/managemcp/scripts/migrate.ts +0 -57
- package/skills/managemcp/scripts/seed.ts +0 -58
- package/skills/managemcp/src/db/index.ts +0 -37
- package/skills/managemcp/src/lib/api-client.ts +0 -108
- package/skills/managemcp/src/lib/config.ts +0 -34
- package/skills/managemcp/src/lib/installer.ts +0 -140
- package/skills/managemcp/src/lib/service-dir.ts +0 -119
- package/skills/sms/scripts/buy-number.ts +0 -63
- package/skills/sms/scripts/test-send.ts +0 -67
- package/skills/sms/src/server.ts +0 -351
- package/skills/sms/src/sse-server.ts +0 -279
- package/skills/transcript/.env.example +0 -10
- package/skills/webcrawling/.env.example +0 -2
package/skills/sms/src/server.ts
DELETED
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* MCP SMS HTTP Server
|
|
4
|
-
* Provides SMS functionality for AI agents via HTTP API using Twilio
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import twilio from "twilio";
|
|
8
|
-
|
|
9
|
-
// Configuration
|
|
10
|
-
const CONFIG = {
|
|
11
|
-
port: parseInt(process.env.PORT || "3848"),
|
|
12
|
-
accountSid: process.env.TWILIO_ACCOUNT_SID || "",
|
|
13
|
-
authToken: process.env.TWILIO_AUTH_TOKEN || "",
|
|
14
|
-
apiKey: process.env.MCP_SMS_API_KEY || "",
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
// Twilio Client
|
|
18
|
-
const twilioClient = twilio(CONFIG.accountSid, CONFIG.authToken);
|
|
19
|
-
|
|
20
|
-
// Helper: Capitalize first letter
|
|
21
|
-
function capitalize(str: string): string {
|
|
22
|
-
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Helper: Get display name for agent - "[Name] (AI Agent)"
|
|
26
|
-
function getDisplayName(agentName: string): string {
|
|
27
|
-
return `${capitalize(agentName)} (AI Agent)`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Helper: Validate API key
|
|
31
|
-
function validateApiKey(request: Request): boolean {
|
|
32
|
-
if (!CONFIG.apiKey) return true;
|
|
33
|
-
const authHeader = request.headers.get("Authorization");
|
|
34
|
-
if (!authHeader) return false;
|
|
35
|
-
const [type, key] = authHeader.split(" ");
|
|
36
|
-
return type === "Bearer" && key === CONFIG.apiKey;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Helper: JSON response
|
|
40
|
-
function jsonResponse(data: any, status = 200): Response {
|
|
41
|
-
return new Response(JSON.stringify(data, null, 2), {
|
|
42
|
-
status,
|
|
43
|
-
headers: { "Content-Type": "application/json" },
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Helper: Error response
|
|
48
|
-
function errorResponse(message: string, status = 400): Response {
|
|
49
|
-
return jsonResponse({ success: false, error: message }, status);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Helper: Format phone number to E.164
|
|
53
|
-
function formatPhoneNumber(phone: string): string {
|
|
54
|
-
const digits = phone.replace(/\D/g, "");
|
|
55
|
-
if (digits.length === 10) {
|
|
56
|
-
return `+1${digits}`;
|
|
57
|
-
}
|
|
58
|
-
if (digits.length === 11 && digits.startsWith("1")) {
|
|
59
|
-
return `+${digits}`;
|
|
60
|
-
}
|
|
61
|
-
if (!phone.startsWith("+")) {
|
|
62
|
-
return `+${digits}`;
|
|
63
|
-
}
|
|
64
|
-
return phone;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Send SMS
|
|
68
|
-
async function sendSms(params: {
|
|
69
|
-
agent_name: string;
|
|
70
|
-
from_number: string;
|
|
71
|
-
to: string;
|
|
72
|
-
body: string;
|
|
73
|
-
}): Promise<{ success: boolean; message_sid?: string; error?: string }> {
|
|
74
|
-
try {
|
|
75
|
-
const message = await twilioClient.messages.create({
|
|
76
|
-
body: params.body,
|
|
77
|
-
from: formatPhoneNumber(params.from_number),
|
|
78
|
-
to: formatPhoneNumber(params.to),
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
success: true,
|
|
83
|
-
message_sid: message.sid,
|
|
84
|
-
};
|
|
85
|
-
} catch (error: any) {
|
|
86
|
-
return {
|
|
87
|
-
success: false,
|
|
88
|
-
error: error.message,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// List messages
|
|
94
|
-
async function listMessages(params: {
|
|
95
|
-
agent_name: string;
|
|
96
|
-
phone_number: string;
|
|
97
|
-
limit?: number;
|
|
98
|
-
direction?: "inbound" | "outbound";
|
|
99
|
-
}): Promise<any> {
|
|
100
|
-
try {
|
|
101
|
-
const options: any = {
|
|
102
|
-
limit: params.limit || 20,
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
if (params.direction === "inbound") {
|
|
106
|
-
options.to = formatPhoneNumber(params.phone_number);
|
|
107
|
-
} else if (params.direction === "outbound") {
|
|
108
|
-
options.from = formatPhoneNumber(params.phone_number);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const messages = await twilioClient.messages.list(options);
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
success: true,
|
|
115
|
-
count: messages.length,
|
|
116
|
-
messages: messages.map((m) => ({
|
|
117
|
-
sid: m.sid,
|
|
118
|
-
from: m.from,
|
|
119
|
-
to: m.to,
|
|
120
|
-
body: m.body,
|
|
121
|
-
status: m.status,
|
|
122
|
-
direction: m.direction,
|
|
123
|
-
date_sent: m.dateSent?.toISOString(),
|
|
124
|
-
date_created: m.dateCreated?.toISOString(),
|
|
125
|
-
})),
|
|
126
|
-
};
|
|
127
|
-
} catch (error: any) {
|
|
128
|
-
return {
|
|
129
|
-
success: false,
|
|
130
|
-
error: error.message,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Get message
|
|
136
|
-
async function getMessage(messageSid: string): Promise<any> {
|
|
137
|
-
try {
|
|
138
|
-
const message = await twilioClient.messages(messageSid).fetch();
|
|
139
|
-
|
|
140
|
-
return {
|
|
141
|
-
success: true,
|
|
142
|
-
sid: message.sid,
|
|
143
|
-
from: message.from,
|
|
144
|
-
to: message.to,
|
|
145
|
-
body: message.body,
|
|
146
|
-
status: message.status,
|
|
147
|
-
direction: message.direction,
|
|
148
|
-
date_sent: message.dateSent?.toISOString(),
|
|
149
|
-
date_created: message.dateCreated?.toISOString(),
|
|
150
|
-
price: message.price,
|
|
151
|
-
price_unit: message.priceUnit,
|
|
152
|
-
};
|
|
153
|
-
} catch (error: any) {
|
|
154
|
-
return {
|
|
155
|
-
success: false,
|
|
156
|
-
error: error.message,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// List phone numbers
|
|
162
|
-
async function listPhoneNumbers(): Promise<any> {
|
|
163
|
-
try {
|
|
164
|
-
const numbers = await twilioClient.incomingPhoneNumbers.list();
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
success: true,
|
|
168
|
-
count: numbers.length,
|
|
169
|
-
phone_numbers: numbers.map((n) => ({
|
|
170
|
-
sid: n.sid,
|
|
171
|
-
phone_number: n.phoneNumber,
|
|
172
|
-
friendly_name: n.friendlyName,
|
|
173
|
-
capabilities: n.capabilities,
|
|
174
|
-
date_created: n.dateCreated?.toISOString(),
|
|
175
|
-
})),
|
|
176
|
-
};
|
|
177
|
-
} catch (error: any) {
|
|
178
|
-
return {
|
|
179
|
-
success: false,
|
|
180
|
-
error: error.message,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Buy phone number
|
|
186
|
-
async function buyPhoneNumber(params: {
|
|
187
|
-
area_code?: string;
|
|
188
|
-
friendly_name?: string;
|
|
189
|
-
}): Promise<any> {
|
|
190
|
-
try {
|
|
191
|
-
// Search for available numbers
|
|
192
|
-
const searchOptions: any = {
|
|
193
|
-
limit: 1,
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
if (params.area_code) {
|
|
197
|
-
searchOptions.areaCode = params.area_code;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const availableNumbers =
|
|
201
|
-
await twilioClient.availablePhoneNumbers("US").local.list(searchOptions);
|
|
202
|
-
|
|
203
|
-
if (availableNumbers.length === 0) {
|
|
204
|
-
return {
|
|
205
|
-
success: false,
|
|
206
|
-
error: "No available phone numbers found",
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Purchase the number
|
|
211
|
-
const purchased = await twilioClient.incomingPhoneNumbers.create({
|
|
212
|
-
phoneNumber: availableNumbers[0].phoneNumber,
|
|
213
|
-
friendlyName: params.friendly_name || "MCP SMS Agent",
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
success: true,
|
|
218
|
-
phone_number: purchased.phoneNumber,
|
|
219
|
-
sid: purchased.sid,
|
|
220
|
-
friendly_name: purchased.friendlyName,
|
|
221
|
-
};
|
|
222
|
-
} catch (error: any) {
|
|
223
|
-
return {
|
|
224
|
-
success: false,
|
|
225
|
-
error: error.message,
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// HTTP Server
|
|
231
|
-
const PATH_PREFIX = "/sms";
|
|
232
|
-
|
|
233
|
-
const server = Bun.serve({
|
|
234
|
-
port: CONFIG.port,
|
|
235
|
-
async fetch(request) {
|
|
236
|
-
const url = new URL(request.url);
|
|
237
|
-
let path = url.pathname;
|
|
238
|
-
const method = request.method;
|
|
239
|
-
|
|
240
|
-
// Strip path prefix if present (for ALB routing)
|
|
241
|
-
if (path.startsWith(PATH_PREFIX)) {
|
|
242
|
-
path = path.slice(PATH_PREFIX.length) || "/";
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Health check
|
|
246
|
-
if (path === "/health" && method === "GET") {
|
|
247
|
-
return jsonResponse({ status: "ok", version: "1.0.0" });
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// API key validation for all other routes
|
|
251
|
-
if (!validateApiKey(request)) {
|
|
252
|
-
return errorResponse("Unauthorized", 401);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
// POST /send - Send SMS
|
|
257
|
-
if (path === "/send" && method === "POST") {
|
|
258
|
-
const body = await request.json();
|
|
259
|
-
|
|
260
|
-
if (!body.agent_name || !body.from_number || !body.to || !body.body) {
|
|
261
|
-
return errorResponse(
|
|
262
|
-
"Missing required fields: agent_name, from_number, to, body"
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const result = await sendSms(body);
|
|
267
|
-
return jsonResponse({
|
|
268
|
-
...result,
|
|
269
|
-
agent: getDisplayName(body.agent_name),
|
|
270
|
-
from: formatPhoneNumber(body.from_number),
|
|
271
|
-
to: formatPhoneNumber(body.to),
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// GET /messages/:phone - List messages for a phone number
|
|
276
|
-
if (path.startsWith("/messages/") && method === "GET") {
|
|
277
|
-
const parts = path.split("/");
|
|
278
|
-
const phoneNumber = decodeURIComponent(parts[2]);
|
|
279
|
-
const agentName = url.searchParams.get("agent") || "unknown";
|
|
280
|
-
const limit = parseInt(url.searchParams.get("limit") || "20");
|
|
281
|
-
const direction = url.searchParams.get("direction") as
|
|
282
|
-
| "inbound"
|
|
283
|
-
| "outbound"
|
|
284
|
-
| undefined;
|
|
285
|
-
|
|
286
|
-
if (!phoneNumber) {
|
|
287
|
-
return errorResponse("Phone number required");
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const result = await listMessages({
|
|
291
|
-
agent_name: agentName,
|
|
292
|
-
phone_number: phoneNumber,
|
|
293
|
-
limit,
|
|
294
|
-
direction,
|
|
295
|
-
});
|
|
296
|
-
return jsonResponse(result);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// GET /message/:sid - Get specific message
|
|
300
|
-
if (path.startsWith("/message/") && method === "GET") {
|
|
301
|
-
const messageSid = path.split("/")[2];
|
|
302
|
-
|
|
303
|
-
if (!messageSid) {
|
|
304
|
-
return errorResponse("Message SID required");
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const result = await getMessage(messageSid);
|
|
308
|
-
return jsonResponse(result);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// GET /numbers - List phone numbers
|
|
312
|
-
if (path === "/numbers" && method === "GET") {
|
|
313
|
-
const result = await listPhoneNumbers();
|
|
314
|
-
return jsonResponse(result);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// POST /numbers/buy - Buy a phone number
|
|
318
|
-
if (path === "/numbers/buy" && method === "POST") {
|
|
319
|
-
const body = await request.json();
|
|
320
|
-
const result = await buyPhoneNumber({
|
|
321
|
-
area_code: body.area_code,
|
|
322
|
-
friendly_name: body.friendly_name,
|
|
323
|
-
});
|
|
324
|
-
return jsonResponse(result);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// GET /agent/:name - Get agent info
|
|
328
|
-
if (path.startsWith("/agent/") && method === "GET") {
|
|
329
|
-
const agentName = path.split("/")[2];
|
|
330
|
-
|
|
331
|
-
if (!agentName) {
|
|
332
|
-
return errorResponse("Agent name required");
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return jsonResponse({
|
|
336
|
-
success: true,
|
|
337
|
-
agent_name: agentName,
|
|
338
|
-
display_name: getDisplayName(agentName),
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return errorResponse("Not found", 404);
|
|
343
|
-
} catch (error: any) {
|
|
344
|
-
console.error("Error:", error);
|
|
345
|
-
return errorResponse(error.message, 500);
|
|
346
|
-
}
|
|
347
|
-
},
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
console.log(`MCP SMS Server running on http://localhost:${CONFIG.port}`);
|
|
351
|
-
console.log(`API Key: ${CONFIG.apiKey ? "configured" : "not configured (open access)"}`);
|
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* MCP SMS SSE Server
|
|
4
|
-
* Provides SMS functionality for AI agents via SSE transport using Twilio
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import express from "express";
|
|
8
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
-
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
10
|
-
import { z } from "zod";
|
|
11
|
-
import twilio from "twilio";
|
|
12
|
-
|
|
13
|
-
// Configuration
|
|
14
|
-
const CONFIG = {
|
|
15
|
-
port: parseInt(process.env.PORT || "3848"),
|
|
16
|
-
accountSid: process.env.TWILIO_ACCOUNT_SID || "",
|
|
17
|
-
authToken: process.env.TWILIO_AUTH_TOKEN || "",
|
|
18
|
-
apiKey: process.env.MCP_SMS_API_KEY || "",
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
// Twilio Client
|
|
22
|
-
const twilioClient = twilio(CONFIG.accountSid, CONFIG.authToken);
|
|
23
|
-
|
|
24
|
-
// Helper functions
|
|
25
|
-
function capitalize(str: string): string {
|
|
26
|
-
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function getDisplayName(agentName: string): string {
|
|
30
|
-
return `${capitalize(agentName)} (AI Agent)`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function formatPhoneNumber(phone: string): string {
|
|
34
|
-
const digits = phone.replace(/\D/g, "");
|
|
35
|
-
if (digits.length === 10) return `+1${digits}`;
|
|
36
|
-
if (digits.length === 11 && digits.startsWith("1")) return `+${digits}`;
|
|
37
|
-
if (!phone.startsWith("+")) return `+${digits}`;
|
|
38
|
-
return phone;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Create MCP Server with tools
|
|
42
|
-
function createMcpServer(): McpServer {
|
|
43
|
-
const server = new McpServer({
|
|
44
|
-
name: "mcp-sms",
|
|
45
|
-
version: "1.0.0",
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// Tool: Send SMS
|
|
49
|
-
server.tool(
|
|
50
|
-
"send_sms",
|
|
51
|
-
"Send an SMS message from an AI agent",
|
|
52
|
-
{
|
|
53
|
-
agent_name: z.string().describe("Name of the AI agent sending the SMS"),
|
|
54
|
-
from_number: z.string().describe("Twilio phone number to send from"),
|
|
55
|
-
to: z.string().describe("Recipient phone number"),
|
|
56
|
-
body: z.string().describe("SMS message body (max 1600 characters)"),
|
|
57
|
-
},
|
|
58
|
-
async ({ agent_name, from_number, to, body }) => {
|
|
59
|
-
try {
|
|
60
|
-
const message = await twilioClient.messages.create({
|
|
61
|
-
body,
|
|
62
|
-
from: formatPhoneNumber(from_number),
|
|
63
|
-
to: formatPhoneNumber(to),
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
content: [{
|
|
68
|
-
type: "text",
|
|
69
|
-
text: JSON.stringify({
|
|
70
|
-
success: true,
|
|
71
|
-
message_sid: message.sid,
|
|
72
|
-
agent: getDisplayName(agent_name),
|
|
73
|
-
from: formatPhoneNumber(from_number),
|
|
74
|
-
to: formatPhoneNumber(to),
|
|
75
|
-
status: message.status,
|
|
76
|
-
}, null, 2),
|
|
77
|
-
}],
|
|
78
|
-
};
|
|
79
|
-
} catch (error: any) {
|
|
80
|
-
return {
|
|
81
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: error.message }, null, 2) }],
|
|
82
|
-
isError: true,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
// Tool: List Messages
|
|
89
|
-
server.tool(
|
|
90
|
-
"list_messages",
|
|
91
|
-
"List SMS messages for a phone number",
|
|
92
|
-
{
|
|
93
|
-
agent_name: z.string().describe("Name of the AI agent"),
|
|
94
|
-
phone_number: z.string().describe("Phone number to list messages for"),
|
|
95
|
-
limit: z.number().optional().default(20).describe("Maximum number of messages"),
|
|
96
|
-
direction: z.enum(["inbound", "outbound"]).optional().describe("Filter by message direction"),
|
|
97
|
-
},
|
|
98
|
-
async ({ agent_name, phone_number, limit, direction }) => {
|
|
99
|
-
try {
|
|
100
|
-
const options: any = { limit };
|
|
101
|
-
if (direction === "inbound") options.to = formatPhoneNumber(phone_number);
|
|
102
|
-
else if (direction === "outbound") options.from = formatPhoneNumber(phone_number);
|
|
103
|
-
|
|
104
|
-
const messages = await twilioClient.messages.list(options);
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
content: [{
|
|
108
|
-
type: "text",
|
|
109
|
-
text: JSON.stringify({
|
|
110
|
-
success: true,
|
|
111
|
-
agent: getDisplayName(agent_name),
|
|
112
|
-
count: messages.length,
|
|
113
|
-
messages: messages.map((m) => ({
|
|
114
|
-
sid: m.sid,
|
|
115
|
-
from: m.from,
|
|
116
|
-
to: m.to,
|
|
117
|
-
body: m.body,
|
|
118
|
-
status: m.status,
|
|
119
|
-
direction: m.direction,
|
|
120
|
-
date_sent: m.dateSent?.toISOString(),
|
|
121
|
-
})),
|
|
122
|
-
}, null, 2),
|
|
123
|
-
}],
|
|
124
|
-
};
|
|
125
|
-
} catch (error: any) {
|
|
126
|
-
return {
|
|
127
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: error.message }, null, 2) }],
|
|
128
|
-
isError: true,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
// Tool: Read Message
|
|
135
|
-
server.tool(
|
|
136
|
-
"read_message",
|
|
137
|
-
"Read a specific SMS message by SID",
|
|
138
|
-
{
|
|
139
|
-
message_sid: z.string().describe("The Twilio message SID"),
|
|
140
|
-
},
|
|
141
|
-
async ({ message_sid }) => {
|
|
142
|
-
try {
|
|
143
|
-
const message = await twilioClient.messages(message_sid).fetch();
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
content: [{
|
|
147
|
-
type: "text",
|
|
148
|
-
text: JSON.stringify({
|
|
149
|
-
success: true,
|
|
150
|
-
sid: message.sid,
|
|
151
|
-
from: message.from,
|
|
152
|
-
to: message.to,
|
|
153
|
-
body: message.body,
|
|
154
|
-
status: message.status,
|
|
155
|
-
direction: message.direction,
|
|
156
|
-
date_sent: message.dateSent?.toISOString(),
|
|
157
|
-
date_created: message.dateCreated?.toISOString(),
|
|
158
|
-
price: message.price,
|
|
159
|
-
price_unit: message.priceUnit,
|
|
160
|
-
}, null, 2),
|
|
161
|
-
}],
|
|
162
|
-
};
|
|
163
|
-
} catch (error: any) {
|
|
164
|
-
return {
|
|
165
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: error.message }, null, 2) }],
|
|
166
|
-
isError: true,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
// Tool: List Phone Numbers
|
|
173
|
-
server.tool(
|
|
174
|
-
"list_phone_numbers",
|
|
175
|
-
"List all Twilio phone numbers in the account",
|
|
176
|
-
{},
|
|
177
|
-
async () => {
|
|
178
|
-
try {
|
|
179
|
-
const numbers = await twilioClient.incomingPhoneNumbers.list();
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
content: [{
|
|
183
|
-
type: "text",
|
|
184
|
-
text: JSON.stringify({
|
|
185
|
-
success: true,
|
|
186
|
-
count: numbers.length,
|
|
187
|
-
phone_numbers: numbers.map((n) => ({
|
|
188
|
-
sid: n.sid,
|
|
189
|
-
phone_number: n.phoneNumber,
|
|
190
|
-
friendly_name: n.friendlyName,
|
|
191
|
-
capabilities: n.capabilities,
|
|
192
|
-
})),
|
|
193
|
-
}, null, 2),
|
|
194
|
-
}],
|
|
195
|
-
};
|
|
196
|
-
} catch (error: any) {
|
|
197
|
-
return {
|
|
198
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: error.message }, null, 2) }],
|
|
199
|
-
isError: true,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
// Tool: Get Agent Info
|
|
206
|
-
server.tool(
|
|
207
|
-
"get_agent_info",
|
|
208
|
-
"Get SMS configuration info for an AI agent",
|
|
209
|
-
{
|
|
210
|
-
agent_name: z.string().describe("Name of the AI agent"),
|
|
211
|
-
},
|
|
212
|
-
async ({ agent_name }) => {
|
|
213
|
-
return {
|
|
214
|
-
content: [{
|
|
215
|
-
type: "text",
|
|
216
|
-
text: JSON.stringify({
|
|
217
|
-
agent_name,
|
|
218
|
-
display_name: getDisplayName(agent_name),
|
|
219
|
-
}, null, 2),
|
|
220
|
-
}],
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
return server;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Validate API key middleware
|
|
229
|
-
function validateApiKey(req: express.Request, res: express.Response, next: express.NextFunction) {
|
|
230
|
-
if (!CONFIG.apiKey) return next();
|
|
231
|
-
const authHeader = req.headers.authorization;
|
|
232
|
-
if (!authHeader) return res.status(401).json({ error: "Unauthorized" });
|
|
233
|
-
const [type, key] = authHeader.split(" ");
|
|
234
|
-
if (type !== "Bearer" || key !== CONFIG.apiKey) return res.status(401).json({ error: "Unauthorized" });
|
|
235
|
-
next();
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Express app
|
|
239
|
-
const app = express();
|
|
240
|
-
// Don't use express.json() globally - SSEServerTransport needs raw stream
|
|
241
|
-
// app.use(express.json());
|
|
242
|
-
|
|
243
|
-
const transports: { [sessionId: string]: SSEServerTransport } = {};
|
|
244
|
-
|
|
245
|
-
app.get("/health", (req, res) => {
|
|
246
|
-
res.json({ status: "ok", version: "1.0.0", transport: "sse" });
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
app.get("/sse", validateApiKey, async (req, res) => {
|
|
250
|
-
console.log("SSE connection request received");
|
|
251
|
-
const transport = new SSEServerTransport("/mcp/sms/messages", res);
|
|
252
|
-
transports[transport.sessionId] = transport;
|
|
253
|
-
console.log("SSE session started:", transport.sessionId);
|
|
254
|
-
|
|
255
|
-
res.on("close", () => {
|
|
256
|
-
console.log("SSE session closed:", transport.sessionId);
|
|
257
|
-
delete transports[transport.sessionId];
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
const server = createMcpServer();
|
|
261
|
-
await server.connect(transport);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
app.post("/messages", validateApiKey, async (req, res) => {
|
|
265
|
-
const sessionId = req.query.sessionId as string;
|
|
266
|
-
const transport = transports[sessionId];
|
|
267
|
-
|
|
268
|
-
if (transport) {
|
|
269
|
-
await transport.handlePostMessage(req, res);
|
|
270
|
-
} else {
|
|
271
|
-
res.status(400).json({ error: "No transport found for sessionId" });
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
app.listen(CONFIG.port, () => {
|
|
276
|
-
console.log(`MCP SMS SSE Server running on http://localhost:${CONFIG.port}`);
|
|
277
|
-
console.log(`SSE endpoint: http://localhost:${CONFIG.port}/sse`);
|
|
278
|
-
console.log(`API Key: ${CONFIG.apiKey ? "configured" : "not configured"}`);
|
|
279
|
-
});
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
# At least one provider API key is required
|
|
2
|
-
|
|
3
|
-
# ElevenLabs - Highest accuracy (96.7%), 3GB files, speaker diarization
|
|
4
|
-
ELEVENLABS_API_KEY=your_elevenlabs_key
|
|
5
|
-
|
|
6
|
-
# OpenAI Whisper - Industry standard, 25MB limit (auto-chunking for larger)
|
|
7
|
-
OPENAI_API_KEY=your_openai_key
|
|
8
|
-
|
|
9
|
-
# Google Gemini - Cost-effective, 2GB files, generous free tier
|
|
10
|
-
GOOGLE_API_KEY=your_google_key
|