Creating Custom MCP Servers

Learn how to build your own MCP servers to extend TeamDay's capabilities with custom integrations and specialized tools.

Getting Started

Prerequisites

  • Node.js 18+ or Python 3.8+
  • Understanding of REST APIs and WebSockets
  • Familiarity with your target integration
  • TeamDay development environment access

Basic MCP Server Structure

// Basic Node.js MCP Server
const express = require('express');
const { MCPServer } = require('@teamday/mcp-sdk');

const app = express();
const mcpServer = new MCPServer({
  name: 'my-custom-server',
  version: '1.0.0',
  description: 'Custom integration server'
});

// Register tools
mcpServer.registerTool('my_tool', {
  description: 'My custom tool',
  parameters: {
    input: { type: 'string', required: true }
  },
  handler: async (params) => {
    // Tool implementation
    return { result: `Processed: ${params.input}` };
  }
});

app.use('/mcp', mcpServer.handler);
app.listen(3000);

Tool Implementation

Simple Tools

// Text processing tool
mcpServer.registerTool('process_text', {
  description: 'Process and transform text',
  parameters: {
    text: { type: 'string', required: true },
    operation: { type: 'string', enum: ['uppercase', 'lowercase', 'reverse'] }
  },
  handler: async ({ text, operation }) => {
    switch (operation) {
      case 'uppercase': return { result: text.toUpperCase() };
      case 'lowercase': return { result: text.toLowerCase() };
      case 'reverse': return { result: text.split('').reverse().join('') };
      default: throw new Error('Invalid operation');
    }
  }
});

Database Tools

// Database query tool
mcpServer.registerTool('query_database', {
  description: 'Execute database queries',
  parameters: {
    query: { type: 'string', required: true },
    params: { type: 'array', default: [] }
  },
  handler: async ({ query, params }) => {
    const db = await getDatabase();
    const result = await db.query(query, params);
    return { data: result.rows, count: result.rowCount };
  }
});

API Integration Tools

// HTTP request tool
mcpServer.registerTool('http_request', {
  description: 'Make HTTP requests to external APIs',
  parameters: {
    url: { type: 'string', required: true },
    method: { type: 'string', default: 'GET' },
    headers: { type: 'object', default: {} },
    body: { type: 'object' }
  },
  handler: async ({ url, method, headers, body }) => {
    const response = await fetch(url, {
      method,
      headers,
      body: body ? JSON.stringify(body) : undefined
    });
    return {
      status: response.status,
      data: await response.json()
    };
  }
});

Configuration and Deployment

Environment Configuration

// config.js
module.exports = {
  port: process.env.PORT || 3000,
  database: {
    host: process.env.DB_HOST,
    port: process.env.DB_PORT,
    name: process.env.DB_NAME,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD
  },
  apis: {
    external_service: {
      url: process.env.EXTERNAL_API_URL,
      key: process.env.EXTERNAL_API_KEY
    }
  }
};

Docker Deployment

FROM node:18-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

Docker Compose

version: '3.8'
services:
  mcp-server:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DB_HOST=database
      - DB_PASSWORD=${DB_PASSWORD}
    depends_on:
      - database
  
  database:
    image: postgres:15
    environment:
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - db_data:/var/lib/postgresql/data

volumes:
  db_data:

Testing and Validation

Unit Testing

// tests/tools.test.js
const { MCPServer } = require('@teamday/mcp-sdk');

describe('Custom Tools', () => {
  let server;
  
  beforeEach(() => {
    server = new MCPServer({ name: 'test-server' });
    // Register tools...
  });

  test('process_text tool', async () => {
    const result = await server.callTool('process_text', {
      text: 'hello',
      operation: 'uppercase'
    });
    expect(result.result).toBe('HELLO');
  });
});

Integration Testing

// Test with TeamDay agent
const testIntegration = async () => {
  const response = await fetch('http://localhost:3000/mcp/tools/my_tool', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ input: 'test data' })
  });
  
  const result = await response.json();
  console.log('Tool result:', result);
};

Security Best Practices

Authentication

// API key authentication
const authenticateRequest = (req, res, next) => {
  const apiKey = req.headers['x-api-key'];
  if (!apiKey || !isValidApiKey(apiKey)) {
    return res.status(401).json({ error: 'Invalid API key' });
  }
  next();
};

app.use('/mcp', authenticateRequest);

Input Validation

const { body, validationResult } = require('express-validator');

app.post('/mcp/tools/:tool',
  body('parameters').isObject(),
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    // Process request...
  }
);

Error Handling

Structured Error Responses

class MCPError extends Error {
  constructor(code, message, details = null) {
    super(message);
    this.code = code;
    this.details = details;
    this.retryable = ['TIMEOUT', 'RATE_LIMIT'].includes(code);
  }
}

// Error handler middleware
app.use((error, req, res, next) => {
  if (error instanceof MCPError) {
    res.status(400).json({
      error: {
        code: error.code,
        message: error.message,
        details: error.details,
        retryable: error.retryable
      }
    });
  } else {
    res.status(500).json({
      error: {
        code: 'INTERNAL_ERROR',
        message: 'Internal server error',
        retryable: false
      }
    });
  }
});

Performance Optimization

Caching

const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 600 }); // 10 minutes

mcpServer.registerTool('cached_api_call', {
  handler: async ({ url }) => {
    const cacheKey = `api:${url}`;
    let result = cache.get(cacheKey);
    
    if (!result) {
      result = await fetch(url).then(r => r.json());
      cache.set(cacheKey, result);
    }
    
    return result;
  }
});

Connection Pooling

const { Pool } = require('pg');
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20,
  idleTimeoutMillis: 30000
});

const getDatabase = () => pool;

Monitoring and Logging

Structured Logging

const winston = require('winston');

const logger = winston.createLogger({
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'mcp-server.log' }),
    new winston.transports.Console()
  ]
});

// Log tool executions
mcpServer.on('tool_execution', (toolName, params, result, duration) => {
  logger.info('Tool executed', {
    tool: toolName,
    duration,
    success: !result.error
  });
});

Health Checks

app.get('/health', async (req, res) => {
  const health = {
    status: 'healthy',
    timestamp: new Date().toISOString(),
    services: {}
  };

  try {
    // Check database
    await pool.query('SELECT 1');
    health.services.database = 'healthy';
  } catch (error) {
    health.services.database = 'unhealthy';
    health.status = 'degraded';
  }

  res.json(health);
});

Publishing Your Server

NPM Package

{
  "name": "@yourorg/mcp-custom-server",
  "version": "1.0.0",
  "description": "Custom MCP server for TeamDay",
  "main": "server.js",
  "keywords": ["mcp", "teamday", "integration"],
  "repository": "https://github.com/yourorg/mcp-custom-server"
}

TeamDay Marketplace

  1. Test thoroughly in development
  2. Create comprehensive documentation
  3. Submit to TeamDay marketplace
  4. Provide support and maintenance

Next Steps