Skip to main content
Glama
palhamel
by palhamel
index.ts13.4 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; import { validateConfig, config } from './config.js'; import { ElksClient } from './elks-client.js'; import { formatErrorResponse, ConfigurationError, handleValidationError } from './errors.js'; import { validatePhoneNumber, validateSmsMessage, validateSenderId } from './validation.js'; import { formatSmsResponse, formatSmsHistory, formatAccountBalance, formatDeliveryStatistics } from './utils.js'; const server = new Server( { name: '46elks-mcp', version: '0.1.0', description: '46elks MCP Server - Send and receive SMS through Swedish telecommunications infrastructure' }, { capabilities: { tools: {} } } ); // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'send_sms', description: 'Send SMS message via 46elks', inputSchema: { type: 'object', properties: { to: { type: 'string', description: 'Recipient phone number with country code - MUST be a real phone number, not a placeholder (e.g., +46XXXXXXXXX for Swedish numbers)' }, message: { type: 'string', description: 'SMS message content (max 160 characters for single SMS)' }, from: { type: 'string', description: 'Sender phone number or name (optional, uses default if not specified)' }, flashsms: { type: 'string', description: 'Set to "yes" for flash SMS that displays immediately and is not stored (optional)' }, dry_run: { type: 'boolean', description: 'Test mode - verify request without sending actual SMS (optional, defaults to environment setting)' } }, required: ['to', 'message'] } }, { name: 'get_sms_messages', description: 'Retrieve SMS message history from 46elks', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Maximum number of messages to retrieve (default: 10, max: 100)', minimum: 1, maximum: 100 }, direction: { type: 'string', enum: ['inbound', 'outbound', 'both'], description: 'Filter messages by direction (default: both)' } }, required: [] } }, { name: 'check_sms_status', description: 'Check delivery status and details of a sent SMS', inputSchema: { type: 'object', properties: { message_id: { type: 'string', description: '46elks message ID returned when SMS was sent' } }, required: ['message_id'] } }, { name: 'check_account_balance', description: 'Check 46elks account balance and account information to verify funds availability for SMS sending', inputSchema: { type: 'object', properties: {}, required: [] } }, { name: 'estimate_sms_cost', description: 'Estimate cost and message segments for SMS without sending it', inputSchema: { type: 'object', properties: { to: { type: 'string', description: 'Recipient phone number with country code (e.g., +46XXXXXXXXX for Swedish numbers)' }, message: { type: 'string', description: 'SMS message content to estimate cost for' }, from: { type: 'string', description: 'Sender phone number or name (optional, uses default if not specified)' } }, required: ['to', 'message'] } }, { name: 'get_delivery_statistics', description: 'Get SMS delivery statistics and success rates from recent messages', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Number of recent messages to analyze for statistics (default: 50, max: 100)', minimum: 10, maximum: 100 } }, required: [] } } ] }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => { try { const { name, arguments: args } = request.params; switch (name) { case 'send_sms': const { to, message, from, flashsms, dry_run } = args as { to: string; message: string; from?: string; flashsms?: string; dry_run?: boolean; }; const isDryRunMode = dry_run !== undefined ? dry_run : config.dryRun; // Validate inputs handleValidationError('phone number', validatePhoneNumber(to)); const messageValidation = validateSmsMessage(message); handleValidationError('message', messageValidation); if (from) { handleValidationError('sender ID', validateSenderId(from)); } // Send SMS via 46elks const elksClient = new ElksClient(); const response = await elksClient.sendSms(to, message, from, dry_run, flashsms); // Format response with validation warnings let responseText = formatSmsResponse(response, isDryRunMode); // Add validation warning if present if (messageValidation.warning) { responseText += `\n\n${messageValidation.warning}`; } return { content: [ { type: 'text', text: responseText } ] }; case 'get_sms_messages': const { limit = 10, direction } = args as { limit?: number; direction?: 'inbound' | 'outbound' | 'both'; }; // Validate limit const messageLimit = Math.min(Math.max(limit, 1), 100); // Get messages via 46elks const elksClientForMessages = new ElksClient(); const messages = await elksClientForMessages.getMessages( messageLimit, direction === 'both' ? undefined : direction ); return { content: [ { type: 'text', text: formatSmsHistory(messages, messageLimit) } ] }; case 'check_sms_status': const { message_id } = args as { message_id: string; }; if (!message_id || typeof message_id !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'message_id is required and must be a string'); } // Get message status via 46elks const elksClientForStatus = new ElksClient(); const messageDetails = await elksClientForStatus.getMessageById(message_id); const cost = messageDetails.cost ? `${messageDetails.cost / 10000} SEK` : 'N/A'; const date = new Date(messageDetails.created).toLocaleString(); const messageDirection = messageDetails.direction === 'outbound' ? '📤 Sent' : '📥 Received'; let statusText = `📱 SMS Status Check\n\n`; statusText += `${messageDirection} Message\n`; statusText += `ID: ${messageDetails.id}\n`; statusText += `Status: ${messageDetails.status}\n`; statusText += `To: ${messageDetails.to}\n`; statusText += `From: ${messageDetails.from}\n`; statusText += `Created: ${date}\n`; statusText += `Cost: ${cost}\n`; statusText += `Message: ${messageDetails.message}`; return { content: [ { type: 'text', text: statusText } ] }; case 'check_account_balance': // Get account information via 46elks const elksClientForAccount = new ElksClient(); const accountInfo = await elksClientForAccount.getAccountInfo(); return { content: [ { type: 'text', text: formatAccountBalance(accountInfo) } ] }; case 'estimate_sms_cost': const { to: estimateTo, message: estimateMessage, from: estimateFrom } = args as { to: string; message: string; from?: string; }; // Validate inputs handleValidationError('phone number', validatePhoneNumber(estimateTo)); const estimateMessageValidation = validateSmsMessage(estimateMessage); handleValidationError('message', estimateMessageValidation); if (estimateFrom) { handleValidationError('sender ID', validateSenderId(estimateFrom)); } // Use dry run to get cost estimate const elksClientForEstimate = new ElksClient(); const estimateResponse = await elksClientForEstimate.sendSms(estimateTo, estimateMessage, estimateFrom, true); const estimatedCost = estimateResponse.estimated_cost ? estimateResponse.estimated_cost / 10000 : 0; const messageLength = estimateMessage.length; const segments = estimateResponse.parts || (messageLength <= 160 ? 1 : Math.ceil(messageLength / 153)); let costEstimateText = `💰 SMS Cost Estimate\n\n`; costEstimateText += `To: ${estimateTo}\n`; costEstimateText += `From: ${estimateResponse.from}\n`; costEstimateText += `Message length: ${messageLength} characters\n`; costEstimateText += `Message segments: ${segments}\n`; costEstimateText += `Estimated cost: ${estimatedCost.toFixed(2)} SEK\n\n`; if (segments > 1) { costEstimateText += `⚠️ Multi-part SMS: This message will be sent as ${segments} parts\n`; costEstimateText += `💡 Tip: Consider shortening to ≤160 characters for single SMS\n\n`; } costEstimateText += `📝 Message preview:\n"${estimateMessage}"\n\n`; // Add validation warning if present if (estimateMessageValidation.warning) { costEstimateText += `${estimateMessageValidation.warning}\n\n`; } costEstimateText += `🧪 This was an estimate only - no SMS was sent`; return { content: [ { type: 'text', text: costEstimateText } ] }; case 'get_delivery_statistics': const { limit: statsLimit = 50 } = args as { limit?: number; }; // Validate and constrain limit const analysisLimit = Math.min(Math.max(statsLimit, 10), 100); // Get messages for analysis - only outbound messages matter for delivery stats const elksClientForStats = new ElksClient(); const messagesForStats = await elksClientForStats.getMessages(analysisLimit); return { content: [ { type: 'text', text: formatDeliveryStatistics(messagesForStats) } ] }; default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { return { content: [formatErrorResponse(error)] }; } }); // Start server async function main() { try { // Validate configuration first validateConfig(); console.error('✓ Configuration validated'); // Show dry run status if (config.dryRun) { console.error('⚠️ DRY RUN mode enabled - SMS messages will NOT be sent'); } else { console.error('📱 Production mode - SMS messages WILL be sent'); } // Test 46elks connection (non-blocking) const elksClient = new ElksClient(); try { await elksClient.testConnection(); console.error('✓ 46elks connection successful'); } catch (error) { console.error('⚠️ 46elks connection test failed - server will start anyway'); console.error(' This is normal if using test credentials or during development'); console.error(' Real API calls will be validated when tools are used'); } // Start MCP server const transport = new StdioServerTransport(); await server.connect(transport); console.error('✓ MCP SMS Server running on stdio'); } catch (error) { if (error instanceof ConfigurationError) { console.error('Configuration Error:', error.message); console.error('\nPlease check your MCP client configuration (Claude Desktop config.json or VS Code mcp.json).'); } else { console.error('Server failed to start:', error instanceof Error ? error.message : error); } process.exit(1); } } main().catch((error) => { console.error('Unexpected error:', error); process.exit(1); });

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/palhamel/46elks-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server