AI Architecture·

Building an MCP Server for Your Business: A Practical Guide to Model Context Protocol

Learn how to build production-ready MCP servers that connect your business systems to AI models — with authentication patterns, tool registration strategies, and real-world implementation patterns that enable powerful context-aware AI applications.

Building an MCP Server for Your Business: A Practical Guide to Model Context Protocol

The Model Context Protocol (MCP) has emerged as the de facto standard for connecting AI models to your business systems. Originally developed by Anthropic, MCP is now widely adopted across the AI ecosystem, enabling seamless integration between large language models and your company's data, tools, and workflows.

In this comprehensive guide, we'll walk through building production-ready MCP servers that unlock the full potential of AI in your business operations.

What Is MCP and Why Does It Matter?

The Model Context Protocol defines a standardized way for AI systems to discover and interact with external capabilities. Think of it as a universal adapter that allows AI models to connect to your databases, APIs, file systems, and business logic without custom integration code for each connection.

Key benefits of MCP:

  • Standardized integration — One protocol, unlimited connectors
  • Type-safe interactions — JSON-RPC 2.0 with JSON Schema validation
  • Security-first design — Built-in authentication and permission scopes
  • Bidirectional communication — AI can both read data and execute actions
  • Vendor agnostic — Works with Claude, GPT, Gemini, and other LLMs

Understanding the MCP Architecture

At its core, MCP follows a client-server pattern:

Transport Layer

  • stdio: Standard input/output for local process communication
  • HTTP with SSE: Server-sent events for remote, stateful connections
  • WebSocket: Full-duplex communication for real-time applications

Protocol Layer

  • JSON-RPC 2.0 message format
  • Request/notification patterns for communication
  • Structured logging and error handling

Capability Layer

  • Tools: Functions AI can call to perform actions
  • Resources: Read-only data sources the AI can access
  • Prompts: Templated interactions for common tasks

Building Your First MCP Server

Let's build a practical MCP server that connects to your company's CRM and provides sales insights to AI assistants.

Setting Up the Project

# Create a new MCP server project
mkdir mcp-crm-server
cd mcp-crm-server
npm init -y
npm install @modelcontextprotocol/sdk zod

# Install additional dependencies for your use case
npm install axios dotenv

Server Structure

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListResourcesRequestSchema,
  ListToolsRequestSchema,
  ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";

// Define your tool schemas
const GetSalesDataSchema = z.object({
  startDate: z.string().datetime(),
  endDate: z.string().datetime(),
  region: z.string().optional(),
});

class CRMMCPServer {
  private server: Server;
  private crmAPI: CRMClient;

  constructor() {
    this.server = new Server(
      {
        name: "crm-mcp-server",
        version: "1.0.0",
      },
      {
        capabilities: {
          tools: {},
          resources: {},
        },
      }
    );

    this.crmAPI = new CRMClient({
      apiKey: process.env.CRM_API_KEY,
      baseURL: process.env.CRM_BASE_URL,
    });

    this.setupHandlers();
  }

  private setupHandlers() {
    // List available tools
    this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools: [
        {
          name: "get_sales_data",
          description: "Get sales performance data for a specified date range",
          inputSchema: {
            type: "object",
            properties: {
              startDate: {
                type: "string",
                format: "date-time",
                description: "Start date in ISO 8601 format",
              },
              endDate: {
                type: "string",
                format: "date-time",
                description: "End date in ISO 8601 format",
              },
              region: {
                type: "string",
                description: "Optional region filter (e.g., 'EMEA', 'APAC')",
              },
            },
            required: ["startDate", "endDate"],
          },
        },
        {
          name: "update_contact_status",
          description: "Update the status of a CRM contact",
          inputSchema: {
            type: "object",
            properties: {
              contactId: {
                type: "string",
                description: "Unique contact identifier",
              },
              status: {
                type: "string",
                enum: ["lead", "qualified", "customer", "churned"],
                description: "New status for the contact",
              },
              notes: {
                type: "string",
                description: "Optional notes about the status change",
              },
            },
            required: ["contactId", "status"],
          },
        },
      ],
    }));

    // Execute tool calls
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name, arguments: args } = request.params;

      try {
        switch (name) {
          case "get_sales_data": {
            const validated = GetSalesDataSchema.parse(args);
            const data = await this.crmAPI.getSalesData(validated);
            return {
              content: [
                {
                  type: "text",
                  text: JSON.stringify(data, null, 2),
                },
              ],
            };
          }

          case "update_contact_status": {
            const { contactId, status, notes } = args as {
              contactId: string;
              status: string;
              notes?: string;
            };
            await this.crmAPI.updateContact(contactId, { status, notes });
            return {
              content: [
                {
                  type: "text",
                  text: `Successfully updated contact ${contactId} to status: ${status}`,
                },
              ],
            };
          }

          default:
            throw new Error(`Unknown tool: ${name}`);
        }
      } catch (error) {
        return {
          content: [
            {
              type: "text",
              text: `Error: ${error instanceof Error ? error.message : String(error)}`,
            },
          ],
          isError: true,
        };
      }
    });

    // List available resources
    this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
      resources: [
        {
          uri: "crm://contacts",
          name: "All CRM Contacts",
          description: "Complete list of contacts in the CRM",
          mimeType: "application/json",
        },
        {
          uri: "crm://deals/pipeline",
          name: "Active Deals Pipeline",
          description: "Current sales pipeline with deal stages",
          mimeType: "application/json",
        },
      ],
    }));

    // Handle resource reads
    this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
      const { uri } = request.params;

      try {
        switch (uri) {
          case "crm://contacts": {
            const contacts = await this.crmAPI.getAllContacts();
            return {
              contents: [
                {
                  uri,
                  mimeType: "application/json",
                  text: JSON.stringify(contacts, null, 2),
                },
              ],
            };
          }

          case "crm://deals/pipeline": {
            const pipeline = await this.crmAPI.getPipeline();
            return {
              contents: [
                {
                  uri,
                  mimeType: "application/json",
                  text: JSON.stringify(pipeline, null, 2),
                },
              ],
            };
          }

          default:
            throw new Error(`Unknown resource: ${uri}`);
        }
      } catch (error) {
        throw new Error(`Failed to read resource: ${error}`);
      }
    });
  }

  async run() {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
    console.error("CRM MCP Server running on stdio");
  }
}

// Start the server
const server = new CRMMCPServer();
server.run().catch(console.error);

Authentication and Security Patterns

Environment-Based Credentials

// config/security.ts
import { z } from "zod";

const SecurityConfigSchema = z.object({
  apiKey: z.string().min(32),
  allowedOrigins: z.array(z.string().url()),
  rateLimit: z.object({
    requestsPerMinute: z.number().default(60),
  }),
  jwtSecret: z.string().optional(),
});

export const securityConfig = SecurityConfigSchema.parse({
  apiKey: process.env.MCP_API_KEY,
  allowedOrigins: process.env.MCP_ALLOWED_ORIGINS?.split(",") || [],
  rateLimit: {
    requestsPerMinute: parseInt(process.env.MCP_RATE_LIMIT || "60"),
  },
  jwtSecret: process.env.MCP_JWT_SECRET,
});

Request Validation Middleware

// middleware/validation.ts
import { z, ZodTypeAny } from "zod";

export function validateToolInput<T extends ZodTypeAny>(
  schema: T,
  input: unknown
): z.infer<T> {
  try {
    return schema.parse(input);
  } catch (error) {
    if (error instanceof z.ZodError) {
      const messages = error.errors
        .map((e) => `${e.path.join(".")}: ${e.message}`)
        .join("; ");
      throw new Error(`Validation failed: ${messages}`);
    }
    throw error;
  }
}

// Sanitize outputs to prevent prompt injection
export function sanitizeOutput(text: string): string {
  return text
    .replace(/<script[^>]*>.*?<\/script>/gi, "")
    .replace(/javascript:/gi, "")
    .replace(/on\w+\s*=/gi, "");
}

Production Deployment Strategies

Docker Containerization

# Dockerfile
FROM node:20-alpine

WORKDIR /app

# Copy package files
COPY package*.json ./
RUN npm ci --only=production

# Copy source code
COPY dist/ ./dist/

# Security: Run as non-root user
USER node

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  CMD node dist/healthcheck.js

CMD ["node", "dist/index.js"]

HTTP Transport with SSE

For remote deployments, use HTTP with Server-Sent Events:

import express from "express";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";

const app = express();
const transport = new SSEServerTransport("/mcp", app);

// Handle MCP connections
app.use("/mcp", async (req, res) => {
  const sessionId = req.headers["x-session-id"] as string;
  await server.connect(transport, { sessionId });
});

// Authentication middleware
app.use((req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader || !verifyToken(authHeader)) {
    return res.status(401).json({ error: "Unauthorized" });
  }
  next();
});

app.listen(3000, () => {
  console.log("MCP Server listening on port 3000");
});

Testing Your MCP Server

Unit Test Example

// tests/crm-server.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";

describe("CRM MCP Server", () => {
  let client: Client;

  beforeEach(async () => {
    const { client: c, server: s } = InMemoryTransport.createLinkedPair();
    client = new Client({ name: "test-client", version: "1.0.0" });
    await client.connect(c);
  });

  it("should list available tools", async () => {
    const tools = await client.listTools();
    expect(tools.tools).toContainEqual(
      expect.objectContaining({ name: "get_sales_data" })
    );
  });

  it("should validate tool input", async () => {
    await expect(
      client.callTool({
        name: "get_sales_data",
        arguments: { startDate: "invalid-date" },
      })
    ).rejects.toThrow();
  });
});

Integration Testing

// tests/integration.test.ts
import { spawn } from "child_process";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

describe("Integration Tests", () => {
  it("should connect to running server", async () => {
    const transport = new StdioClientTransport({
      command: "node",
      args: ["dist/index.js"],
      env: { CRM_API_KEY: "test-key" },
    });

    const client = new Client({
      name: "integration-test",
      version: "1.0.0",
    });

    await client.connect(transport);

    const resources = await client.listResources();
    expect(resources.resources.length).toBeGreaterThan(0);

    await client.close();
  });
});

Common Patterns and Best Practices

1. Progressive Capability Disclosure

Don't expose all capabilities at once. Implement access control:

getCapabilities() {
  const capabilities: Capabilities = {};
  
  if (this.hasPermission("tools:read")) {
    capabilities.tools = this.getToolsForUser();
  }
  
  if (this.hasPermission("resources:read")) {
    capabilities.resources = this.getResourcesForUser();
  }
  
  if (this.hasPermission("prompts:read")) {
    capabilities.prompts = this.getPromptsForUser();
  }
  
  return capabilities;
}

2. Pagination for Large Datasets

{
  name: "search_contacts",
  inputSchema: {
    type: "object",
    properties: {
      query: { type: "string" },
      limit: { type: "number", maximum: 100, default: 20 },
      cursor: { type: "string", description: "Pagination cursor" },
    },
  },
}

3. Error Handling with Context

class MCPToolError extends Error {
  constructor(
    message: string,
    public code: string,
    public details?: Record<string, unknown>
  ) {
    super(message);
  }
}

// In tool handler
try {
  await riskyOperation();
} catch (error) {
  throw new MCPToolError(
    "Failed to update contact",
    "CONTACT_UPDATE_FAILED",
    { contactId: args.contactId, reason: error.message }
  );
}

Monitoring and Observability

Structured Logging

import { pino } from "pino";

const logger = pino({
  level: process.env.LOG_LEVEL || "info",
  formatters: {
    bindings: () => ({ service: "mcp-server" }),
  },
});

// In your handlers
logger.info({ tool: name, duration_ms: elapsed }, "Tool executed successfully");
logger.error({ error, tool: name }, "Tool execution failed");

Health Checks

// healthcheck.ts
import { checkDatabaseConnection } from "./db";
import { checkExternalAPIs } from "./apis";

export async function healthCheck(): Promise<HealthStatus> {
  const [db, apis] = await Promise.all([
    checkDatabaseConnection(),
    checkExternalAPIs(),
  ]);

  const healthy = db.healthy && apis.every((a) => a.healthy);

  return {
    status: healthy ? "healthy" : "unhealthy",
    timestamp: new Date().toISOString(),
    checks: {
      database: db,
      external_apis: apis,
    },
  };
}

Real-World Use Cases

Multi-System Integration

// Connect CRM, ERP, and Support systems
class EnterpriseMCPServer {
  private integrations = {
    crm: new CRMIntegration(),
    erp: new ERPIntegration(),
    support: new SupportIntegration(),
  };

  async getCustomer360View(customerId: string) {
    const [crmData, erpData, supportHistory] = await Promise.all([
      this.integrations.crm.getCustomer(customerId),
      this.integrations.erp.getOrders(customerId),
      this.integrations.support.getTickets(customerId),
    ]);

    return {
      profile: crmData,
      purchaseHistory: erpData,
      supportInteractions: supportHistory,
      healthScore: this.calculateHealthScore(crmData, erpData, supportHistory),
    };
  }
}

File Processing Pipeline

{
  name: "process_document",
  description: "Upload and process a document",
  inputSchema: {
    type: "object",
    properties: {
      file_path: { type: "string" },
      operations: {
        type: "array",
        items: {
          enum: ["ocr", "summarize", "extract_entities", "translate"],
        },
      },
    },
  },
}

Conclusion

Building MCP servers unlocks the full potential of AI in your business by providing secure, standardized access to your systems. Start with a single integration, follow security best practices, and gradually expand your capabilities. The investment in proper architecture pays dividends through faster AI feature development and maintainable integrations.

Remember: MCP is about enabling AI to work with your business, not replacing your existing systems. Focus on clear use cases, robust error handling, and comprehensive monitoring for production success.


Ready to implement MCP in your business? Contact Tropical Media at tropical-media.work for expert guidance on AI architecture and integration.