Skip to main content

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:

  1. Go to AI Assistants → Edit assistant
  2. Agent tabMCP Servers
  3. Click Add MCP Server
  4. 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
  5. 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

  1. Make a test call to your assistant
  2. Ask a question that requires your MCP server
  3. Check logs for:
    • Incoming requests
    • Query execution
    • Response sent
  4. 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 }
});
}