MCP Servers: Context Distribution for AI Tools

Model Context Protocol (MCP) servers are solving the context distribution problem that's plagued AI tooling since day one. Instead of every AI application building its own context management, MCP creates a standardized way for AI tools to access external context sources.

I've built and deployed several MCP servers in production, and the pattern is transformative when done right. MCP servers turn context from a custom integration nightmare into a plug-and-play architecture.

The Context Distribution Problem

Every AI application needs context from external sources—databases, APIs, files, services. The traditional approach means building custom integrations for each context source in each AI application:

  • Claude Desktop needs file system access? Custom file integration.
  • Cursor needs project context? Custom project scanning.
  • Custom AI app needs database access? Custom database integration.

This creates massive duplication and maintenance overhead. Every AI tool rebuilds the same context access patterns.

MCP servers solve this by creating standardized context endpoints that any MCP-compatible AI tool can consume. Build the integration once, use it everywhere.

MCP Architecture: How It Actually Works

MCP uses a client-server model where AI applications (clients) connect to context sources (servers) through a standardized protocol:

AI Client (Claude, Cursor, etc.)
    ↓ MCP Protocol
MCP Server (your context source)
    ↓ Native integration
External System (database, API, filesystem)

The MCP server acts as a translation layer between the standardized MCP protocol and your specific context source.

MCP Protocol Basics

MCP defines three main concepts:

  • Resources: Context data that can be read (files, database records, API responses)
  • Tools: Actions that can be performed (database queries, API calls, file operations)
  • Prompts: Pre-defined prompt templates with dynamic context injection

This covers the main ways AI applications need to interact with external context.

Building Your First MCP Server

Let's build a practical MCP server that provides context from a project's git repository:

import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { CallToolRequestSchema, ListResourcesRequestSchema } from '@modelcontextprotocol/sdk/types.js'
import { execSync } from 'child_process'
import fs from 'fs'

const server = new Server(
  {
    name: 'git-context-server',
    version: '1.0.0',
  },
  {
    capabilities: {
      resources: {},
      tools: {},
    },
  }
)

// Define resources (git repository context)
server.setRequestHandler(ListResourcesRequestSchema, async () => {
  return {
    resources: [
      {
        uri: 'git://commits/recent',
        name: 'Recent Git Commits',
        description: 'Last 10 commits with messages and changes',
        mimeType: 'application/json',
      },
      {
        uri: 'git://status',
        name: 'Git Status',
        description: 'Current working directory status',
        mimeType: 'text/plain',
      },
      {
        uri: 'git://branches',
        name: 'Git Branches',
        description: 'All local and remote branches',
        mimeType: 'application/json',
      },
    ],
  }
})

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

  switch (uri) {
    case 'git://commits/recent':
      const commits = execSync('git log --oneline -10 --format="%h|%an|%ad|%s" --date=short', 
        { encoding: 'utf-8' })
        .split('\n')
        .filter(line => line.trim())
        .map(line => {
          const [hash, author, date, message] = line.split('|')
          return { hash, author, date, message }
        })
      
      return {
        contents: [{
          uri,
          mimeType: 'application/json',
          text: JSON.stringify(commits, null, 2),
        }],
      }

    case 'git://status':
      const status = execSync('git status --porcelain', { encoding: 'utf-8' })
      return {
        contents: [{
          uri,
          mimeType: 'text/plain',
          text: status || 'Working directory clean',
        }],
      }

    case 'git://branches':
      const branches = execSync('git branch -a', { encoding: 'utf-8' })
        .split('\n')
        .map(branch => branch.trim().replace(/^\*\s*/, ''))
        .filter(branch => branch)
      
      return {
        contents: [{
          uri,
          mimeType: 'application/json',
          text: JSON.stringify(branches, null, 2),
        }],
      }

    default:
      throw new Error(`Unknown resource: ${uri}`)
  }
})

This server provides git context as MCP resources that any compatible AI client can access.

Advanced MCP Server Patterns

Dynamic Context Based on User Intent

Smart MCP servers provide different context based on what the AI is trying to do:

// Context selection based on AI request patterns
server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
  const context = request.meta?.context || {}
  const userQuery = context.lastUserMessage || ''
  
  // Provide different resources based on apparent user intent
  if (userQuery.includes('debug') || userQuery.includes('error')) {
    return {
      resources: [
        { uri: 'logs://errors', name: 'Recent Errors' },
        { uri: 'logs://debug', name: 'Debug Logs' },
        { uri: 'monitoring://alerts', name: 'Active Alerts' },
      ]
    }
  } else if (userQuery.includes('performance') || userQuery.includes('slow')) {
    return {
      resources: [
        { uri: 'metrics://performance', name: 'Performance Metrics' },
        { uri: 'profiling://recent', name: 'Recent Profiles' },
      ]
    }
  } else {
    return {
      resources: [
        { uri: 'project://overview', name: 'Project Overview' },
        { uri: 'git://commits/recent', name: 'Recent Changes' },
      ]
    }
  }
})

Context Caching and Performance

MCP servers should cache expensive context operations:

class CachedMCPServer {
  constructor() {
    this.cache = new Map()
    this.cacheTimeout = 5 * 60 * 1000 // 5 minutes
  }

  async getResource(uri) {
    const cacheKey = uri
    const cached = this.cache.get(cacheKey)
    
    if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
      return cached.data
    }

    const data = await this.fetchResource(uri)
    this.cache.set(cacheKey, {
      data,
      timestamp: Date.now(),
    })

    return data
  }

  async fetchResource(uri) {
    // Expensive operation to fetch actual resource
    switch (uri) {
      case 'database://user-activity':
        return await this.queryDatabase('SELECT * FROM user_activity ORDER BY timestamp DESC LIMIT 100')
      case 'api://external-service':
        return await this.callExternalAPI('/api/v1/data')
      default:
        throw new Error(`Unknown resource: ${uri}`)
    }
  }
}

Context Security in MCP Servers

MCP servers need robust security since they bridge external context with AI applications:

Access Control

class SecureMCPServer {
  constructor(config) {
    this.permissions = config.permissions
    this.allowedClients = config.allowedClients
  }

  async handleRequest(request, clientInfo) {
    // Verify client is allowed to connect
    if (!this.allowedClients.includes(clientInfo.clientId)) {
      throw new Error('Unauthorized client')
    }

    // Check resource-level permissions
    if (request.params.uri) {
      const resource = this.parseResourceUri(request.params.uri)
      if (!this.hasPermission(clientInfo.userId, resource.type, 'read')) {
        throw new Error(`No read permission for ${resource.type}`)
      }
    }

    return await this.processRequest(request)
  }

  hasPermission(userId, resourceType, action) {
    const userPermissions = this.permissions[userId] || []
    return userPermissions.some(p => 
      p.resource === resourceType && p.actions.includes(action)
    )
  }
}

Context Sanitization

Always sanitize sensitive information before providing context to AI applications:

function sanitizeContext(data, contextType) {
  switch (contextType) {
    case 'database-records':
      return data.map(record => ({
        ...record,
        // Remove sensitive fields
        password: undefined,
        ssn: undefined,
        credit_card: undefined,
        // Mask email domains for privacy
        email: record.email ? maskEmail(record.email) : undefined,
      }))
      
    case 'log-entries':
      return data.map(entry => ({
        ...entry,
        // Remove API keys, tokens, passwords from log messages
        message: entry.message
          .replace(/api_key=[\w\-]+/gi, 'api_key=***')
          .replace(/token=[\w\-]+/gi, 'token=***')
          .replace(/password=[\w\-]+/gi, 'password=***'),
      }))
      
    default:
      return data
  }
}

function maskEmail(email) {
  const [local, domain] = email.split('@')
  return `${local.slice(0, 2)}***@${domain}`
}

Production MCP Server Architecture

Production MCP servers need more than basic functionality:

Multi-Tenant Context

class MultiTenantMCPServer {
  constructor() {
    this.tenantContexts = new Map()
  }

  async getContextForTenant(tenantId, resourceType) {
    const tenantContext = this.tenantContexts.get(tenantId) || {
      databases: {},
      apis: {},
      files: {},
    }

    switch (resourceType) {
      case 'database':
        return await this.getDatabaseContext(tenantContext.databases)
      case 'api':
        return await this.getAPIContext(tenantContext.apis)
      case 'files':
        return await this.getFileContext(tenantContext.files)
      default:
        throw new Error(`Unsupported resource type: ${resourceType}`)
    }
  }

  async initializeTenant(tenantId, config) {
    this.tenantContexts.set(tenantId, {
      databases: {
        connectionString: config.db_connection,
        schema: config.db_schema,
      },
      apis: {
        baseUrl: config.api_base_url,
        apiKey: config.api_key,
      },
      files: {
        basePath: config.file_base_path,
        permissions: config.file_permissions,
      },
    })
  }
}

Context Transformation Pipelines

Sometimes raw context needs transformation for AI consumption:

class ContextTransformationPipeline {
  constructor() {
    this.transformers = new Map()
  }

  addTransformer(resourceType, transformer) {
    this.transformers.set(resourceType, transformer)
  }

  async transformContext(uri, rawData) {
    const resourceType = this.parseResourceType(uri)
    const transformer = this.transformers.get(resourceType)
    
    if (!transformer) {
      return rawData // Return raw data if no transformer
    }

    return await transformer.transform(rawData)
  }
}

// Example transformers
const databaseTransformer = {
  async transform(rows) {
    return {
      summary: `Found ${rows.length} records`,
      recentActivity: rows.slice(0, 10).map(row => ({
        timestamp: row.created_at,
        user: row.user_name,
        action: row.action_type,
      })),
      patterns: this.analyzePatterns(rows),
    }
  },

  analyzePatterns(rows) {
    // Extract useful patterns for AI analysis
    const userActions = {}
    rows.forEach(row => {
      userActions[row.user_id] = (userActions[row.user_id] || 0) + 1
    })
    
    return {
      mostActiveUsers: Object.entries(userActions)
        .sort(([,a], [,b]) => b - a)
        .slice(0, 5)
        .map(([userId, count]) => ({ userId, actionCount: count })),
    }
  }
}

MCP Server Discovery and Registry

As teams build multiple MCP servers, discovery becomes important:

// MCP Server Registry
class MCPRegistry {
  constructor() {
    this.servers = new Map()
  }

  registerServer(serverId, config) {
    this.servers.set(serverId, {
      id: serverId,
      name: config.name,
      description: config.description,
      capabilities: config.capabilities,
      endpoint: config.endpoint,
      healthCheck: config.healthCheck,
      lastSeen: Date.now(),
    })
  }

  async discoverServers(query) {
    const results = []
    
    for (const [serverId, server] of this.servers) {
      if (this.matchesQuery(server, query)) {
        const isHealthy = await this.checkHealth(server)
        if (isHealthy) {
          results.push(server)
        }
      }
    }

    return results.sort((a, b) => b.lastSeen - a.lastSeen)
  }

  matchesQuery(server, query) {
    const searchTerms = query.toLowerCase().split(' ')
    const searchable = `${server.name} ${server.description}`.toLowerCase()
    
    return searchTerms.every(term => searchable.includes(term))
  }

  async checkHealth(server) {
    try {
      const response = await fetch(server.healthCheck, { timeout: 5000 })
      return response.ok
    } catch {
      return false
    }
  }
}

Testing MCP Servers

MCP servers need comprehensive testing since they bridge external systems:

// Test framework for MCP servers
class MCPServerTester {
  constructor(server) {
    this.server = server
  }

  async testResourceAccess() {
    const resources = await this.server.listResources()
    
    for (const resource of resources.resources) {
      const content = await this.server.readResource(resource.uri)
      
      // Validate resource content structure
      assert(content.contents?.length > 0, `Resource ${resource.uri} returned no content`)
      assert(content.contents[0].text || content.contents[0].blob, 
        `Resource ${resource.uri} has no text or blob content`)
      
      // Validate MIME type consistency
      if (resource.mimeType === 'application/json') {
        assert(() => JSON.parse(content.contents[0].text), 
          `Resource ${resource.uri} claims to be JSON but isn't valid`)
      }
    }
  }

  async testToolExecution() {
    const tools = await this.server.listTools()
    
    for (const tool of tools.tools) {
      // Test with valid parameters
      const testParams = this.generateTestParams(tool.inputSchema)
      const result = await this.server.callTool(tool.name, testParams)
      
      assert(result.content?.length > 0, `Tool ${tool.name} returned no content`)
      
      // Test error handling with invalid parameters
      const invalidParams = this.generateInvalidParams(tool.inputSchema)
      await assert.rejects(
        () => this.server.callTool(tool.name, invalidParams),
        `Tool ${tool.name} should reject invalid parameters`
      )
    }
  }

  generateTestParams(inputSchema) {
    // Generate valid test parameters based on JSON schema
    const params = {}
    for (const [key, prop] of Object.entries(inputSchema.properties)) {
      switch (prop.type) {
        case 'string':
          params[key] = 'test-value'
          break
        case 'number':
          params[key] = 42
          break
        case 'boolean':
          params[key] = true
          break
        case 'array':
          params[key] = ['test-item']
          break
        default:
          params[key] = 'test-value'
      }
    }
    return params
  }
}

MCP Server Monitoring

Production MCP servers need monitoring and observability:

class MCPServerMonitoring {
  constructor() {
    this.metrics = {
      requestCount: 0,
      errorCount: 0,
      responseTime: [],
      resourceHits: new Map(),
    }
  }

  instrumentServer(server) {
    const originalListResources = server.listResources
    const originalReadResource = server.readResource
    const originalCallTool = server.callTool

    server.listResources = async (request) => {
      const start = Date.now()
      try {
        const result = await originalListResources.call(server, request)
        this.recordSuccess('listResources', Date.now() - start)
        return result
      } catch (error) {
        this.recordError('listResources', error)
        throw error
      }
    }

    server.readResource = async (request) => {
      const start = Date.now()
      const { uri } = request.params
      
      try {
        const result = await originalReadResource.call(server, request)
        this.recordSuccess('readResource', Date.now() - start)
        this.recordResourceHit(uri)
        return result
      } catch (error) {
        this.recordError('readResource', error)
        throw error
      }
    }
  }

  recordSuccess(operation, responseTime) {
    this.metrics.requestCount++
    this.metrics.responseTime.push(responseTime)
    
    // Keep only last 1000 response times for memory efficiency
    if (this.metrics.responseTime.length > 1000) {
      this.metrics.responseTime = this.metrics.responseTime.slice(-1000)
    }
  }

  recordError(operation, error) {
    this.metrics.errorCount++
    console.error(`MCP Server error in ${operation}:`, error)
  }

  recordResourceHit(uri) {
    const count = this.metrics.resourceHits.get(uri) || 0
    this.metrics.resourceHits.set(uri, count + 1)
  }

  getMetrics() {
    const avgResponseTime = this.metrics.responseTime.length > 0
      ? this.metrics.responseTime.reduce((a, b) => a + b) / this.metrics.responseTime.length
      : 0

    return {
      totalRequests: this.metrics.requestCount,
      errorCount: this.metrics.errorCount,
      errorRate: this.metrics.requestCount > 0 
        ? this.metrics.errorCount / this.metrics.requestCount 
        : 0,
      avgResponseTime,
      popularResources: Array.from(this.metrics.resourceHits.entries())
        .sort(([,a], [,b]) => b - a)
        .slice(0, 10)
        .map(([uri, count]) => ({ uri, hitCount: count })),
    }
  }
}

The Strategic Impact of MCP

MCP servers change how teams think about AI context architecture:

  • Centralized context management: Build context integrations once, use across all AI tools
  • Standardized access patterns: All context sources exposed through consistent interfaces
  • Better security boundaries: Context access controlled at the MCP server level
  • Context observability: Monitor and optimize context usage across applications

Teams that embrace MCP early will have significant advantages in AI tooling velocity. Instead of building custom context integrations for each AI application, they build reusable MCP servers that work with any compatible AI tool.

Think of MCP servers as microservices for AI context. They provide focused, well-defined interfaces that can be developed, deployed, and maintained independently while serving multiple AI applications.

The future of AI tooling is standardized context distribution. MCP servers make context sharing as straightforward as REST APIs made web service integration.

Related