index.ts•5.31 kB
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { ClickSendClient } from './client.js';
import { validateSMSParams, validateTTSParams, ValidationError } from './utils/validation.js';
const CLICKSEND_USERNAME = process.env.CLICKSEND_USERNAME;
const CLICKSEND_API_KEY = process.env.CLICKSEND_API_KEY;
if (!CLICKSEND_USERNAME || !CLICKSEND_API_KEY) {
throw new Error('CLICKSEND_USERNAME and CLICKSEND_API_KEY environment variables are required');
}
class ClickSendServer {
private server: Server;
private client: ClickSendClient;
private lastActionTime: number = 0;
private readonly RATE_LIMIT_MS = 12000; // 5 actions per minute
constructor() {
this.server = new Server(
{
name: 'clicksend-mcp',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.client = new ClickSendClient(CLICKSEND_USERNAME || "", CLICKSEND_API_KEY || "");
this.setupToolHandlers();
// Error handling
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
private checkRateLimit() {
const now = Date.now();
if (now - this.lastActionTime < this.RATE_LIMIT_MS) {
throw new McpError(
ErrorCode.InvalidRequest,
`Rate limit exceeded. Please wait ${Math.ceil((this.RATE_LIMIT_MS - (now - this.lastActionTime)) / 1000)} seconds.`
);
}
this.lastActionTime = now;
}
private setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'send_sms',
description: 'Send SMS messages via ClickSend',
inputSchema: {
type: 'object',
properties: {
to: {
type: 'string',
description: 'Phone number in E.164 format (e.g. +61423456789)'
},
message: {
type: 'string',
description: 'Message content to send'
}
},
required: ['to', 'message'],
additionalProperties: false
}
},
{
name: 'make_tts_call',
description: 'Make Text-to-Speech calls via ClickSend',
inputSchema: {
type: 'object',
properties: {
to: {
type: 'string',
description: 'Phone number in E.164 format'
},
message: {
type: 'string',
description: 'Text content to convert to speech'
},
voice: {
type: 'string',
enum: ['female', 'male'],
default: 'female',
description: 'Voice type for TTS'
}
},
required: ['to', 'message'],
additionalProperties: false
}
}
]
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
this.checkRateLimit();
switch (request.params.name) {
case 'send_sms': {
const { to, message } = request.params.arguments as { to: string; message: string };
validateSMSParams(to, message);
const result = await this.client.sendSMS({ to, message });
return {
content: [
{
type: 'text',
text: `SMS sent successfully: ${JSON.stringify(result)}`
}
]
};
}
case 'make_tts_call': {
const { to, message, voice } = request.params.arguments as {
to: string;
message: string;
voice?: 'male' | 'female'
};
validateTTSParams(to, message, voice);
const result = await this.client.makeTTSCall({ to, message, voice });
return {
content: [
{
type: 'text',
text: `TTS call initiated successfully: ${JSON.stringify(result)}`
}
]
};
}
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
} catch (error) {
if (error instanceof ValidationError) {
throw new McpError(ErrorCode.InvalidParams, error.message);
}
if (error instanceof McpError) {
throw error;
}
throw new McpError(
ErrorCode.InternalError,
error instanceof Error ? error.message : 'Unknown error occurred'
);
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('ClickSend MCP server running on stdio');
}
}
const server = new ClickSendServer();
server.run().catch(console.error);