Building Custom MCP Servers
Learn how to build your own MCP server to connect Voka AI assistants to proprietary systems, custom databases, or specialized business tools.
When to Build a Custom MCP Server
Build a custom MCP server when:
- ✅ You need to connect to an internal company system
- ✅ The platform you use isn't supported by Voka AI's built-in integrations
- ✅ You have specialized business logic or workflows
- ✅ You want to expose custom APIs to your assistant
Don't build a custom MCP server if:
- ❌ The platform is already supported (Acuity, Square, Jobber, etc.)
- ❌ A community MCP server exists for your use case
- ❌ A simple webhook would work instead
Prerequisites
You'll need:
- Basic programming knowledge (Node.js, Python, or similar)
- Understanding of REST APIs or databases you're connecting to
- Server or hosting environment to run the MCP server
- API credentials for systems you're integrating
Recommended skills:
- HTTP/REST API design
- Authentication (OAuth, API keys)
- Error handling and logging
- Security best practices
MCP Server Architecture
A custom MCP server has three main components:
1. Transport Layer
Handles communication between Voka AI and your server:
- HTTPS endpoint - Receives requests from Voka AI
- Authentication - Validates requests
- Request routing - Directs queries to appropriate handlers
2. Business Logic
Your custom integration code:
- Query handlers - Process information requests
- Action handlers - Execute operations (create, update, delete)
- Data transformation - Format responses for the AI assistant
3. External Connections
Integration with your systems:
- Database queries - PostgreSQL, MySQL, MongoDB, etc.
- API calls - Third-party services
- File operations - Read/write to storage
- Custom logic - Business-specific workflows
Quick Start Example
Here's a minimal custom MCP server in Node.js that queries a PostgreSQL database:
const express = require('express');
const { Pool } = require('pg');
const app = express();
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});
app.use(express.json());
// Authentication middleware
app.use((req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (apiKey !== process.env.MCP_API_KEY) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
});
// Query handler
app.post('/query', async (req, res) => {
const { query, params } = req.body;
try {
const result = await pool.query(query, params);
res.json({
success: true,
data: result.rows
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
// Action handler
app.post('/action', async (req, res) => {
const { action, params } = req.body;
try {
let result;
switch(action) {
case 'create_customer':
result = await pool.query(
'INSERT INTO customers (name, email, phone) VALUES ($1, $2, $3) RETURNING *',
[params.name, params.email, params.phone]
);
break;
default:
return res.status(400).json({ error: 'Unknown action' });
}
res.json({
success: true,
data: result.rows[0]
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
app.listen(3000, () => {
console.log('MCP server running on port 3000');
});
MCP Protocol Standards
Your custom server must follow the MCP protocol specification:
Request Format
{
"method": "query" | "action",
"params": {
"query": "SELECT * FROM customers WHERE id = $1",
"args": [123]
}
}
Response Format
Success:
{
"success": true,
"data": {
"id": 123,
"name": "John Smith",
"email": "[email protected]"
}
}
Error:
{
"success": false,
"error": "Customer not found"
}
Security Best Practices
Authentication
- ✅ Use API keys or OAuth tokens
- ✅ Rotate keys regularly
- ✅ Validate all incoming requests
- ✅ Use HTTPS only (never HTTP)
Data Protection
- ✅ Sanitize all inputs (prevent SQL injection)
- ✅ Validate data types and formats
- ✅ Limit query results (prevent data leaks)
- ✅ Log access attempts
Rate Limiting
- ✅ Implement rate limits per API key
- ✅ Throttle suspicious requests
- ✅ Set timeout limits
- ✅ Monitor usage patterns
Example rate limiting:
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each API key to 100 requests per windowMs
});
app.use('/query', limiter);
app.use('/action', limiter);
Deploying Your MCP Server
Option 1: Cloud Hosting
Popular platforms:
- Heroku - Easy deployment, managed infrastructure
- AWS Lambda - Serverless, pay-per-use
- Google Cloud Run - Containerized, auto-scaling
- DigitalOcean - Simple VPS hosting
Option 2: On-Premise
Run on your own servers:
- More control over security
- No external dependencies
- Requires VPN or public IP
- More maintenance overhead
Option 3: Docker Container
Package as a Docker image:
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Connecting to Voka AI
Once deployed, add your custom MCP server to Voka AI:
- Go to AI Assistants → Edit assistant
- Agent tab → MCP Servers
- Click Add MCP Server
- Enter:
- Name: Descriptive name (e.g., "Company Database")
- URL: Your server URL (e.g.,
https://mcp.yourcompany.com) - API Key: Your MCP_API_KEY value
- Save and test
Testing Your MCP Server
Local Testing
Use curl to test locally:
curl -X POST http://localhost:3000/query \
-H "Content-Type: application/json" \
-H "x-api-key: your_api_key" \
-d '{"query": "SELECT * FROM customers LIMIT 5"}'
Integration Testing
- Make a test call to your assistant
- Ask a question that requires your MCP server
- Check logs for:
- Incoming requests
- Query execution
- Response sent
- Review call transcript for correct behavior
Monitoring and Maintenance
Logging
Log all requests and errors:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
app.use((req, res, next) => {
logger.info(`${req.method} ${req.path}`, {
headers: req.headers,
body: req.body
});
next();
});
Error Handling
Always return useful error messages:
app.use((err, req, res, next) => {
logger.error('Unhandled error', { error: err.message, stack: err.stack });
res.status(500).json({
success: false,
error: 'Internal server error',
message: process.env.NODE_ENV === 'development' ? err.message : undefined
});
});
Health Checks
Add a health endpoint:
app.get('/health', async (req, res) => {
try {
// Check database connection
await pool.query('SELECT 1');
res.json({ status: 'healthy', timestamp: new Date() });
} catch (error) {
res.status(503).json({ status: 'unhealthy', error: error.message });
}
});
Example Use Cases
Customer Database Lookup
Query customer information during calls:
app.post('/query', async (req, res) => {
const { phone_number } = req.body.params;
const result = await pool.query(
'SELECT name, account_status, last_purchase FROM customers WHERE phone = $1',
[phone_number]
);
res.json({ success: true, data: result.rows[0] });
});
Inventory Check
Check product availability:
app.post('/query', async (req, res) => {
const { product_id } = req.body.params;
const result = await pool.query(
'SELECT quantity, location FROM inventory WHERE product_id = $1',
[product_id]
);
res.json({ success: true, data: result.rows[0] });
});
Order Creation
Create orders from phone calls:
app.post('/action', async (req, res) => {
const { customer_id, items } = req.body.params;
const result = await pool.query(
'INSERT INTO orders (customer_id, items, status) VALUES ($1, $2, $3) RETURNING *',
[customer_id, JSON.stringify(items), 'pending']
);
res.json({ success: true, data: result.rows[0] });
});
Troubleshooting
"Connection Timeout"
- Check firewall rules allow Voka AI's IP addresses
- Verify server is running and accessible
- Test with curl from external location
"Authentication Failed"
- Verify API key matches exactly
- Check header name is correct (
x-api-key) - Ensure no extra whitespace in keys
"Slow Responses"
- Add database indexes for common queries
- Implement caching for repeated data
- Use connection pooling
- Consider CDN for static data
Advanced Topics
Caching Responses
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 600 }); // 10 min cache
app.post('/query', async (req, res) => {
const cacheKey = JSON.stringify(req.body);
const cached = cache.get(cacheKey);
if (cached) {
return res.json(cached);
}
const result = await executeQuery(req.body);
cache.set(cacheKey, result);
res.json(result);
});
Webhooks Integration
Allow your MCP server to send updates to Voka AI:
const axios = require('axios');
async function notifyVokaAI(event, data) {
await axios.post(process.env.VOKA_WEBHOOK_URL, {
event,
data,
timestamp: new Date()
}, {
headers: { 'x-api-key': process.env.VOKA_API_KEY }
});
}