suarify-mcp-server 0.1.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.
Files changed (3) hide show
  1. package/README.md +73 -0
  2. package/index.js +395 -0
  3. package/package.json +42 -0
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # Suarify MCP Server
2
+
3
+ This is a Model Context Protocol (MCP) server for Suarify, providing tools for AI agents to interact with Suarify's voice calling and lead management platform.
4
+
5
+ ## Features
6
+
7
+ - **Voice Calls**: Initiate outbound calls and configure inbound settings.
8
+ - **Lead Management**: CRUD operations for leads and bulk uploads.
9
+ - **Agent Configuration**: Manage AI voice agents and phone configurations.
10
+ - **Modern MCP Design**: Returns `structuredContent` for LLM efficiency and uses standardized naming.
11
+
12
+ ## Prerequisites
13
+
14
+ - Node.js 20+
15
+ - [Suarify](https://suarify.my) API Key
16
+
17
+ ## Configuration
18
+
19
+ Set the following environment variables:
20
+
21
+ - `SUARIFY_API_KEY`: (Required) Your API key for authentication.
22
+ - `SUARIFY_BASE_URL`: (Optional) Defaults to `https://suarify1.my`.
23
+
24
+ ## Usage
25
+
26
+ ### Local Development
27
+
28
+ 1. Install dependencies:
29
+ ```bash
30
+ npm install
31
+ ```
32
+ 2. Run tests:
33
+ ```bash
34
+ npm test
35
+ ```
36
+ 3. Start the server (stdio transport):
37
+ ```bash
38
+ export SUARIFY_API_KEY=your_key_here
39
+ node index.js
40
+ ```
41
+
42
+ ### Running with Docker
43
+
44
+ 1. **Build the image**:
45
+ ```bash
46
+ docker build -t suarify-mcp .
47
+ ```
48
+
49
+ 2. **Run the container**:
50
+ ```bash
51
+ docker run -e SUARIFY_API_KEY=your_key_here suarify-mcp
52
+ ```
53
+
54
+ ## Tools Overview
55
+
56
+ All tools are prefixed with `suarify_` and use `snake_case`.
57
+
58
+ | Tool Name | Description |
59
+ |-----------|-------------|
60
+ | `suarify_initiate_call` | Start a simple voice call. |
61
+ | `suarify_do_outbound_call` | Detailed outbound call with validation. |
62
+ | `suarify_setup_inbound_settings` | Configure AI behavior for inbound calls. |
63
+ | `suarify_list_leads` / `suarify_create_lead` | Manage your lead database. |
64
+ | `suarify_list_user_agents` | Retrieve AI voice agents. |
65
+ | `suarify_get_outbound_call_logs` | Fetch historical call data. |
66
+
67
+ ## Development
68
+
69
+ The project uses Jest for unit testing. Tool logic is extracted into a `handlers` object for easy isolation.
70
+
71
+ ```bash
72
+ npm test
73
+ ```
package/index.js ADDED
@@ -0,0 +1,395 @@
1
+ #!/usr/bin/env node
2
+ import { FastMCP } from "fastmcp";
3
+ import axios, { AxiosError } from "axios";
4
+ import { z } from "zod";
5
+ import url from "url";
6
+
7
+ // --- SILENCE STDOUT ---
8
+ // MCP uses stdout for communication. Any other output will break the protocol.
9
+ const originalError = console.error;
10
+
11
+ // Redirect all standard console methods to stderr
12
+ console.log = (...args) => originalError(...args);
13
+ console.info = (...args) => originalError(...args);
14
+ console.warn = (...args) => originalError(...args);
15
+
16
+ // Patch process.stdout.write to ensure ONLY JSON messages (likely from FastMCP) go to stdout
17
+ const stdoutWrite = process.stdout.write;
18
+ process.stdout.write = function (chunk) {
19
+ const str = chunk.toString();
20
+ if (str.trim().startsWith('{') || str.trim().startsWith('[')) {
21
+ return stdoutWrite.apply(process.stdout, arguments);
22
+ }
23
+ return process.stderr.write.apply(process.stderr, arguments);
24
+ };
25
+
26
+ const BASE_URL = process.env.SUARIFY_BASE_URL || "https://suarify.my";
27
+ const API_KEY = process.env.SUARIFY_API_KEY;
28
+
29
+ if (!API_KEY) {
30
+ originalError("CRITICAL: SUARIFY_API_KEY environment variable is not set. Please sign up at https://suarify.my/register-new-user to get your API key.");
31
+ }
32
+
33
+ // --- Server Definition ---
34
+ const mcp = new FastMCP("suarify-mcp-server", {
35
+ version: "0.1.0"
36
+ });
37
+
38
+ const apiClient = axios.create({
39
+ baseURL: BASE_URL,
40
+ headers: {
41
+ "Content-Type": "application/json",
42
+ "x-api-key": API_KEY,
43
+ },
44
+ });
45
+
46
+ apiClient.interceptors.response.use(
47
+ (response) => response,
48
+ (error) => {
49
+ if (error.response && (error.response.status === 401 || error.response.status === 403)) {
50
+ const signupMsg = " (API authentication failed. Please ensure your SUARIFY_API_KEY is valid or sign up at https://suarify.my/register-new-user)";
51
+ error.message += signupMsg;
52
+ }
53
+ return Promise.reject(error);
54
+ }
55
+ );
56
+
57
+ // --- Helpers ---
58
+ function formatError(error) {
59
+ if (error instanceof AxiosError) {
60
+ const status = error.response?.status;
61
+ const data = error.response?.data;
62
+ let msg = `API Error (${status || 'Network'}): ${error.message}`;
63
+ if (data && typeof data === 'object') {
64
+ msg += ` - ${JSON.stringify(data)}`;
65
+ }
66
+ return {
67
+ isError: true,
68
+ content: [{ type: "text", text: msg }],
69
+ structuredContent: { error: msg, status, data }
70
+ };
71
+ }
72
+ const msg = `Unexpected Error: ${error instanceof Error ? error.message : String(error)}`;
73
+ return {
74
+ isError: true,
75
+ content: [{ type: "text", text: msg }],
76
+ structuredContent: { error: msg }
77
+ };
78
+ }
79
+
80
+ // --- Handlers ---
81
+ export const handlers = {
82
+ setupInboundSettings: async (args) => {
83
+ try {
84
+ const response = await apiClient.post("/inbound-phone-settings", args);
85
+ return {
86
+ content: [{ type: "text", text: `Inbound settings configured for ${args.phonenumber}` }],
87
+ structuredContent: response.data
88
+ };
89
+ } catch (e) { return formatError(e); }
90
+ },
91
+ getInboundSettings: async (args) => {
92
+ try {
93
+ const response = await apiClient.get("/inbound-phone-settings", { params: args });
94
+ return {
95
+ content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
96
+ structuredContent: response.data
97
+ };
98
+ } catch (e) { return formatError(e); }
99
+ },
100
+ setupPhoneConfiguration: async (args) => {
101
+ try {
102
+ const response = await apiClient.post("/api/phone-configuration", args);
103
+ return {
104
+ content: [{ type: "text", text: `Phone configuration upserted for token: ${args.tokenid}` }],
105
+ structuredContent: response.data
106
+ };
107
+ } catch (e) { return formatError(e); }
108
+ },
109
+ getPhoneConfiguration: async (args) => {
110
+ try {
111
+ const response = await apiClient.get("/api/phone-configuration", { params: args });
112
+ return {
113
+ content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
114
+ structuredContent: response.data
115
+ };
116
+ } catch (e) { return formatError(e); }
117
+ },
118
+ initiateCall: async (args) => {
119
+ try {
120
+ const response = await apiClient.post("/api/call", args);
121
+ return {
122
+ content: [{ type: "text", text: `Call initiated: ${JSON.stringify(response.data)}` }],
123
+ structuredContent: response.data
124
+ };
125
+ } catch (e) { return formatError(e); }
126
+ },
127
+ doOutboundCall: async (args) => {
128
+ try {
129
+ const response = await apiClient.post("/do-outbound-phone-call", args);
130
+ return {
131
+ content: [{ type: "text", text: `Outbound call executed: ${JSON.stringify(response.data)}` }],
132
+ structuredContent: response.data
133
+ };
134
+ } catch (e) { return formatError(e); }
135
+ },
136
+ getOutboundCallLogs: async (args) => {
137
+ try {
138
+ const response = await apiClient.get("/api/outbound-call-logs", { params: args });
139
+ return {
140
+ content: [{ type: "text", text: `Retrieved ${response.data?.length || 0} outbound logs.` }],
141
+ structuredContent: response.data
142
+ };
143
+ } catch (e) { return formatError(e); }
144
+ },
145
+ getInboundCallLogs: async (args) => {
146
+ try {
147
+ const response = await apiClient.get("/api/inbound-call-logs", { params: args });
148
+ return {
149
+ content: [{ type: "text", text: `Retrieved ${response.data?.length || 0} inbound logs.` }],
150
+ structuredContent: response.data
151
+ };
152
+ } catch (e) { return formatError(e); }
153
+ },
154
+ listUserAgents: async (args) => {
155
+ try {
156
+ const response = await apiClient.get("/api/user-agents", { params: args });
157
+ return {
158
+ content: [{ type: "text", text: `Retrieved ${response.data?.length || 0} user agents.` }],
159
+ structuredContent: response.data
160
+ };
161
+ } catch (e) { return formatError(e); }
162
+ },
163
+ getUserAgent: async (args) => {
164
+ try {
165
+ const { id, ...params } = args;
166
+ const response = await apiClient.get(`/api/user-agents/${id}`, { params });
167
+ return {
168
+ content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
169
+ structuredContent: response.data
170
+ };
171
+ } catch (e) { return formatError(e); }
172
+ },
173
+ deleteUserAgent: async (args) => {
174
+ try {
175
+ const { id, ...params } = args;
176
+ const response = await apiClient.delete(`/api/user-agents/${id}`, { params });
177
+ return {
178
+ content: [{ type: "text", text: `User agent ${id} deleted.` }],
179
+ structuredContent: response.data
180
+ };
181
+ } catch (e) { return formatError(e); }
182
+ },
183
+ createLead: async (args) => {
184
+ try {
185
+ const response = await apiClient.post("/api/user-leads", args);
186
+ return {
187
+ content: [{ type: "text", text: `Lead created for ${args.receipient_name}` }],
188
+ structuredContent: response.data
189
+ };
190
+ } catch (e) { return formatError(e); }
191
+ },
192
+ bulkUploadLeads: async (args) => {
193
+ try {
194
+ const response = await apiClient.post("/api/user-leads/bulk", args);
195
+ return {
196
+ content: [{ type: "text", text: `Bulk upload processed: ${JSON.stringify(response.data)}` }],
197
+ structuredContent: response.data
198
+ };
199
+ } catch (e) { return formatError(e); }
200
+ },
201
+ listLeads: async (args) => {
202
+ try {
203
+ const response = await apiClient.get("/api/user-leads", { params: args });
204
+ return {
205
+ content: [{ type: "text", text: `Retrieved ${response.data?.length || 0} leads.` }],
206
+ structuredContent: response.data
207
+ };
208
+ } catch (e) { return formatError(e); }
209
+ },
210
+ getLead: async (args) => {
211
+ try {
212
+ const response = await apiClient.get(`/api/user-leads/${args.id}`);
213
+ return {
214
+ content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
215
+ structuredContent: response.data
216
+ };
217
+ } catch (e) { return formatError(e); }
218
+ },
219
+ updateLead: async (args) => {
220
+ try {
221
+ const { id, ...data } = args;
222
+ const response = await apiClient.patch(`/api/user-leads/${id}`, data);
223
+ return {
224
+ content: [{ type: "text", text: `Lead ${id} updated.` }],
225
+ structuredContent: response.data
226
+ };
227
+ } catch (e) { return formatError(e); }
228
+ },
229
+ deleteLead: async (args) => {
230
+ try {
231
+ const response = await apiClient.delete(`/api/user-leads/${args.id}`);
232
+ return {
233
+ content: [{ type: "text", text: `Lead ${args.id} deleted.` }],
234
+ structuredContent: response.data
235
+ };
236
+ } catch (e) { return formatError(e); }
237
+ },
238
+ };
239
+
240
+ // --- Inbound Phone Settings ---
241
+
242
+ mcp.addTool({
243
+ name: "suarify_setup_inbound_settings",
244
+ description: "Configure the AI agent behavior, voice, and prompts for inbound calls on a specific phone number. This setup is stored as 'inbound-<phonenumber>'. Requires Suarify API Key.",
245
+ parameters: z.object({
246
+ phonenumber: z.string().describe("The phone number to configure (e.g., 015487666768)"),
247
+ params: z.string().describe("JSON string containing main_voice, start_time, owner_email, start_message, system_prompt, planned_call_id, transfer_message"),
248
+ openai_key: z.string().optional().describe("Optional OpenAI API key override"),
249
+ }),
250
+ execute: handlers.setupInboundSettings,
251
+ });
252
+
253
+ mcp.addTool({
254
+ name: "suarify_get_inbound_settings",
255
+ description: "Get the current inbound call settings for the authenticated user's default phone number.",
256
+ parameters: z.object({
257
+ owner_email: z.string().optional().describe("Owner email (required if using system API key)"),
258
+ }),
259
+ execute: handlers.getInboundSettings,
260
+ });
261
+
262
+ // --- Generic Phone Configuration ---
263
+
264
+ mcp.addTool({
265
+ name: "suarify_setup_phone_configuration",
266
+ description: "Upsert general phone configuration in the call_params table. Use this for custom tokenized configurations.",
267
+ parameters: z.object({
268
+ tokenid: z.string().describe("Unique token identifier for this configuration"),
269
+ params: z.any().describe("Configuration parameters object or JSON string"),
270
+ openai_key: z.string().optional().describe("Optional OpenAI API key override"),
271
+ }),
272
+ execute: handlers.setupPhoneConfiguration,
273
+ });
274
+
275
+ mcp.addTool({
276
+ name: "suarify_get_phone_configuration",
277
+ description: "Retrieve a specific phone configuration by tokenid, or list all configurations.",
278
+ parameters: z.object({
279
+ tokenid: z.string().optional().describe("Specific token ID to retrieve. If omitted, returns all (max 100)."),
280
+ }),
281
+ execute: handlers.getPhoneConfiguration,
282
+ });
283
+
284
+ // --- Outbound Calls ---
285
+
286
+ mcp.addTool({
287
+ name: "suarify_initiate_call",
288
+ description: "Initiate an outbound AI voice call using simple or enhanced parameters.",
289
+ parameters: z.object({
290
+ phone_number: z.string().optional().describe("Target phone number (simple format)"),
291
+ system_prompt: z.string().optional().describe("System prompt for AI (simple format)"),
292
+ start_message: z.string().optional().describe("Initial greeting message (simple format)"),
293
+ voice: z.string().optional().describe("Voice selection (simple format)"),
294
+ receipient_phone: z.string().optional().describe("Target phone number (enhanced format)"),
295
+ agent_prompt: z.string().optional().describe("AI instructions (enhanced format)"),
296
+ }),
297
+ execute: handlers.initiateCall,
298
+ });
299
+
300
+ mcp.addTool({
301
+ name: "suarify_do_outbound_call",
302
+ description: "Make an outbound phone call with full validation (balance check, user profile). Required for production calls.",
303
+ parameters: z.object({
304
+ owner_email: z.string().describe("Owner's email address"),
305
+ password: z.string().describe("Must be 'LIVE' to execute the call"),
306
+ receipient_phone: z.string().describe("Target phone number (international format)"),
307
+ agent_voice: z.string().optional().describe("Voice selection"),
308
+ agent_prompt: z.string().optional().describe("System prompt"),
309
+ agent_start_message: z.string().optional().describe("First message agent speaks"),
310
+ }),
311
+ execute: handlers.doOutboundCall,
312
+ });
313
+
314
+ // --- Call Logs ---
315
+
316
+ mcp.addTool({
317
+ name: "suarify_get_outbound_call_logs",
318
+ description: "Get all outbound call logs with optional filters and record limit.",
319
+ parameters: z.object({
320
+ current_phone_number: z.string().describe("Filter by the outbound phone number used"),
321
+ totalNumberOfRecords: z.number().optional().default(50).describe("Number of records to return"),
322
+ }),
323
+ execute: handlers.getOutboundCallLogs,
324
+ });
325
+
326
+ mcp.addTool({
327
+ name: "suarify_get_inbound_call_logs",
328
+ description: "Get all inbound call logs with optional filters and record limit.",
329
+ parameters: z.object({
330
+ current_phone_number: z.string().describe("Filter by the inbound phone number"),
331
+ totalNumberOfRecords: z.number().optional().default(50).describe("Number of records to return"),
332
+ }),
333
+ execute: handlers.getInboundCallLogs,
334
+ });
335
+
336
+ // --- User Agents ---
337
+
338
+ mcp.addTool({
339
+ name: "suarify_list_user_agents",
340
+ description: "Retrieve all AI agents for the authenticated user.",
341
+ parameters: z.object({
342
+ owner_email: z.string().optional().describe("Owner email filter"),
343
+ limit: z.number().optional().default(100),
344
+ }),
345
+ execute: handlers.listUserAgents,
346
+ });
347
+
348
+ mcp.addTool({
349
+ name: "suarify_get_user_agent",
350
+ description: "Retrieve a single AI agent by ID.",
351
+ parameters: z.object({
352
+ id: z.string().describe("The ID of the user agent"),
353
+ }),
354
+ execute: handlers.getUserAgent,
355
+ });
356
+
357
+ // --- Leads Management ---
358
+
359
+ mcp.addTool({
360
+ name: "suarify_create_lead",
361
+ description: "Create a single lead record with detailed recipient information.",
362
+ parameters: z.object({
363
+ owner_email: z.string().describe("Owner's email address"),
364
+ receipient_name: z.string().describe("Name of the recipient"),
365
+ receipient_phone: z.string().describe("Phone number of the recipient"),
366
+ }),
367
+ execute: handlers.createLead,
368
+ });
369
+
370
+ mcp.addTool({
371
+ name: "suarify_list_leads",
372
+ description: "Retrieve a list of leads with optional filtering and pagination.",
373
+ parameters: z.object({
374
+ owner_email: z.string().optional().describe("Filter by owner email"),
375
+ limit: z.number().optional().default(100),
376
+ }),
377
+ execute: handlers.listLeads,
378
+ });
379
+
380
+ mcp.addTool({
381
+ name: "suarify_delete_lead",
382
+ description: "Remove a lead record from the database.",
383
+ parameters: z.object({
384
+ id: z.string().describe("The ID of the lead record"),
385
+ }),
386
+ execute: handlers.deleteLead,
387
+ });
388
+
389
+ export { mcp, apiClient };
390
+
391
+ if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
392
+ mcp.start({
393
+ transportType: "stdio",
394
+ });
395
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "suarify-mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "Suarify MCP Server for voice calling and lead management",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "suarify-mcp-server": "index.js"
9
+ },
10
+ "scripts": {
11
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
12
+ "start": "node index.js"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/suarifymy/mcp-suarify-server.git"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "suarify",
22
+ "voice-ai",
23
+ "telephony"
24
+ ],
25
+ "author": "Suarify",
26
+ "license": "ISC",
27
+ "dependencies": {
28
+ "axios": "^1.13.5",
29
+ "dotenv": "^17.2.4",
30
+ "fastmcp": "^3.32.0",
31
+ "zod": "^4.3.6"
32
+ },
33
+ "devDependencies": {
34
+ "axios-mock-adapter": "^2.1.0",
35
+ "jest": "^30.2.0"
36
+ },
37
+ "files": [
38
+ "index.js",
39
+ "README.md",
40
+ "package.json"
41
+ ]
42
+ }