4.2 The Model Context Protocol (MCP)

🎯 Learning Objectives

  • Understand the Model Context Protocol architecture and design principles
  • Implement MCP servers and clients for different use cases
  • Learn transport mechanisms and communication patterns
  • Build production-ready MCP integrations

🔌 What is Model Context Protocol?

The Model Context Protocol (MCP) is an open standard that enables secure, controlled connections between AI models and external data sources and tools. Think of it as a universal adapter that allows AI assistants to safely access and interact with any system while maintaining security and user control.

MCP Architecture Overview

AI Client
Transport Layer
MCP Server
External System

Secure, standardized communication between AI models and external resources

🔧 Key Benefits of MCP

  • Standardization: Universal protocol for AI-tool integration
  • Security: Controlled access with permission management
  • Flexibility: Works with any data source or API
  • Scalability: Handle multiple concurrent connections
  • Interoperability: Cross-platform and vendor-agnostic

📊 MCP Protocol Stack

MCP Communication Layers

Application Layer
AI Client Applications (Claude Desktop, VS Code, etc.)
MCP Protocol Layer
JSON-RPC 2.0 based messaging, resource discovery, tool execution
Transport Layer
stdio, SSE (Server-Sent Events), WebSocket connections
Network Layer
Local processes, HTTP/HTTPS, secure tunneling

🏗️ Core Components

  • Resources: Data that can be read (files, databases, APIs)
  • Tools: Functions that can be executed
  • Prompts: Reusable prompt templates
  • Sampling: Model completion requests

🔄 Communication Flow

  • Initialization: Capability negotiation
  • Discovery: List available resources/tools
  • Request: Execute tools or read resources
  • Response: Return results or errors

🛠️ Building an MCP Server

Let's create a practical MCP server that provides file system access and basic tools. This example shows the complete implementation pattern:

MCP Server Manifest (package.json)

{ "name": "filesystem-mcp-server", "version": "1.0.0", "description": "MCP server providing filesystem access", "main": "dist/index.js", "type": "module", "scripts": { "build": "tsc", "start": "node dist/index.js" }, "dependencies": { "@modelcontextprotocol/sdk": "^0.1.0", "fs-extra": "^11.1.1", "path": "^0.12.7" }, "devDependencies": { "typescript": "^5.0.0", "@types/node": "^20.0.0" } }

TypeScript MCP Server Implementation

import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import fs from 'fs-extra'; import path from 'path'; class FileSystemMCPServer { private server: Server; private allowedPaths: string[]; constructor(allowedPaths: string[] = []) { this.allowedPaths = allowedPaths; this.server = new Server( { name: "filesystem-server", version: "1.0.0" }, { capabilities: { resources: {}, tools: {} } } ); this.setupResourceHandlers(); this.setupToolHandlers(); } private isPathAllowed(filePath: string): boolean { const resolvedPath = path.resolve(filePath); return this.allowedPaths.some(allowedPath => resolvedPath.startsWith(path.resolve(allowedPath)) ); } private setupResourceHandlers() { // List available file resources this.server.setRequestHandler('resources/list', async () => { const resources = []; for (const allowedPath of this.allowedPaths) { try { const files = await fs.readdir(allowedPath); for (const file of files) { const filePath = path.join(allowedPath, file); const stat = await fs.stat(filePath); if (stat.isFile()) { resources.push({ uri: `file://${filePath}`, name: file, description: `File: ${file}`, mimeType: this.getMimeType(file) }); } } } catch (error) { // Skip inaccessible directories } } return { resources }; }); // Read file content this.server.setRequestHandler('resources/read', async (request) => { const { uri } = request.params; if (!uri.startsWith('file://')) { throw new Error('Only file:// URIs are supported'); } const filePath = uri.slice(7); // Remove 'file://' prefix if (!this.isPathAllowed(filePath)) { throw new Error('Access denied to this path'); } try { const content = await fs.readFile(filePath, 'utf8'); return { contents: [{ uri, mimeType: this.getMimeType(filePath), text: content }] }; } catch (error) { throw new Error(`Failed to read file: ${error.message}`); } }); } private setupToolHandlers() { // List available tools this.server.setRequestHandler('tools/list', async () => { return { tools: [ { name: "write_file", description: "Write content to a file", inputSchema: { type: "object", properties: { path: { type: "string", description: "File path" }, content: { type: "string", description: "File content" } }, required: ["path", "content"] } }, { name: "create_directory", description: "Create a new directory", inputSchema: { type: "object", properties: { path: { type: "string", description: "Directory path" } }, required: ["path"] } }, { name: "list_directory", description: "List directory contents", inputSchema: { type: "object", properties: { path: { type: "string", description: "Directory path" } }, required: ["path"] } } ] }; }); // Execute tools this.server.setRequestHandler('tools/call', async (request) => { const { name, arguments: args } = request.params; switch (name) { case 'write_file': return await this.writeFile(args.path, args.content); case 'create_directory': return await this.createDirectory(args.path); case 'list_directory': return await this.listDirectory(args.path); default: throw new Error(`Unknown tool: ${name}`); } }); } private async writeFile(filePath: string, content: string) { if (!this.isPathAllowed(filePath)) { throw new Error('Access denied to this path'); } await fs.writeFile(filePath, content, 'utf8'); return { content: [{ type: "text", text: `Successfully wrote ${content.length} characters to ${filePath}` }] }; } private async createDirectory(dirPath: string) { if (!this.isPathAllowed(dirPath)) { throw new Error('Access denied to this path'); } await fs.ensureDir(dirPath); return { content: [{ type: "text", text: `Successfully created directory: ${dirPath}` }] }; } private async listDirectory(dirPath: string) { if (!this.isPathAllowed(dirPath)) { throw new Error('Access denied to this path'); } const files = await fs.readdir(dirPath, { withFileTypes: true }); const fileList = files.map(file => `${file.isDirectory() ? '📁' : '📄'} ${file.name}` ).join('\n'); return { content: [{ type: "text", text: `Directory contents of ${dirPath}:\n${fileList}` }] }; } private getMimeType(filename: string): string { const ext = path.extname(filename).toLowerCase(); const mimeTypes = { '.txt': 'text/plain', '.md': 'text/markdown', '.json': 'application/json', '.js': 'text/javascript', '.ts': 'text/typescript', '.py': 'text/x-python' }; return mimeTypes[ext] || 'text/plain'; } public async start() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Filesystem MCP Server started'); } } // Start the server with environment configuration const allowedPaths = process.env.ALLOWED_PATHS?.split(':') || [process.cwd()]; const server = new FileSystemMCPServer(allowedPaths); server.start();

📱 MCP Client Integration

Here's how to integrate with an MCP server from a client application:

Client Configuration Example

// MCP Client Configuration (claude_desktop_config.json) { "mcpServers": { "filesystem": { "command": "node", "args": [ "path/to/filesystem-server/dist/index.js" ], "env": { "ALLOWED_PATHS": "/home/user/documents:/home/user/projects" } }, "database": { "command": "python", "args": [ "database-server.py" ], "env": { "DB_CONNECTION_STRING": "postgresql://user:pass@localhost/db" } }, "web-search": { "command": "npx", "args": [ "@modelcontextprotocol/server-web-search" ], "env": { "SEARCH_API_KEY": "your-api-key" } } } }

JavaScript MCP Client Implementation

import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { spawn } from 'child_process'; class MCPClientManager { private clients: Map<string, Client> = new Map(); async connectToServer(serverName: string, config: any) { // Spawn the MCP server process const serverProcess = spawn( config.command, config.args || [], { env: { ...process.env, ...config.env }, stdio: ['pipe', 'pipe', 'inherit'] } ); // Create client and transport const transport = new StdioClientTransport({ readable: serverProcess.stdout!, writable: serverProcess.stdin! }); const client = new Client( { name: `${serverName}-client`, version: "1.0.0" }, { capabilities: {} } ); await client.connect(transport); this.clients.set(serverName, client); return client; } async listResources(serverName: string) { const client = this.clients.get(serverName); if (!client) { throw new Error(`Client ${serverName} not connected`); } return await client.request('resources/list', {}); } async readResource(serverName: string, uri: string) { const client = this.clients.get(serverName); if (!client) { throw new Error(`Client ${serverName} not connected`); } return await client.request('resources/read', { uri }); } async listTools(serverName: string) { const client = this.clients.get(serverName); if (!client) { throw new Error(`Client ${serverName} not connected`); } return await client.request('tools/list', {}); } async callTool(serverName: string, toolName: string, args: any) { const client = this.clients.get(serverName); if (!client) { throw new Error(`Client ${serverName} not connected`); } return await client.request('tools/call', { name: toolName, arguments: args }); } } // Example usage async demonstrateMCPUsage() { const manager = new MCPClientManager(); // Connect to filesystem server await manager.connectToServer('filesystem', { command: 'node', args: ['filesystem-server.js'], env: { ALLOWED_PATHS: '/tmp:/home/user/documents' } }); // List available resources const resources = await manager.listResources('filesystem'); console.log('Available resources:', resources); // Read a file const fileContent = await manager.readResource( 'filesystem', 'file:///tmp/example.txt' ); console.log('File content:', fileContent); // Use a tool const result = await manager.callTool( 'filesystem', 'write_file', { path: '/tmp/output.txt', content: 'Hello from MCP!' } ); console.log('Tool result:', result); }

🎯 Popular MCP Server Examples

📁 Filesystem Server

Purpose: File operations and directory management

  • Read/write files with permission control
  • Directory listing and navigation
  • File search and filtering
  • Batch operations
Use Cases: Code editing, document management, log analysis

🗄️ Database Server

Purpose: Database query and management

  • SQL query execution
  • Schema introspection
  • Data visualization
  • Performance monitoring
Use Cases: Data analysis, reporting, database administration

🌐 Web API Server

Purpose: External API integration

  • REST API calls
  • Authentication management
  • Rate limiting
  • Response caching
Use Cases: Web scraping, service integration, data fetching

🔧 Custom Tool Server

Purpose: Domain-specific operations

  • Business logic execution
  • Workflow automation
  • System monitoring
  • Custom calculations
Use Cases: Enterprise tools, specialized workflows

🎮 MCP Communication Demo

🎮 Try MCP Protocol Messages

Click the buttons below to see MCP protocol messages in action:

Click a button above to see MCP protocol messages...

⚖️ MCP vs Alternative Approaches

Approach Standardization Security Performance Flexibility Ecosystem
MCP ✓ Universal Standard ✓ Built-in Security ✓ Efficient ✓ Very Flexible ✓ Growing Fast
Custom APIs ✗ No Standard △ Variable ✓ Can Be Fast ✓ Full Control ✗ Fragmented
Function Calling △ Provider-specific △ Model-dependent ✓ Direct ✗ Limited △ Moderate
Webhooks ✗ No Standard △ Variable △ Async △ Limited ✓ Widespread
Plugin Systems △ Platform-specific △ Sandboxed △ Variable △ Constrained △ Platform-bound

🏆 MCP Development Best Practices

🔒 Security Considerations
  • Implement strict path validation and sandboxing
  • Use principle of least privilege for permissions
  • Validate all input parameters thoroughly
  • Log security events and access attempts
  • Implement rate limiting and timeout mechanisms
⚡ Performance Optimization
  • Cache frequently accessed resources
  • Use streaming for large data transfers
  • Implement connection pooling
  • Monitor resource usage and memory leaks
  • Use async/await patterns consistently
🔧 Error Handling
  • Provide detailed error messages with context
  • Implement graceful degradation
  • Use structured error responses
  • Handle network failures and timeouts
  • Implement retry mechanisms with backoff
📝 Development & Testing
  • Write comprehensive integration tests
  • Document all resources and tools clearly
  • Version your MCP server APIs
  • Use TypeScript for better type safety
  • Implement health checks and monitoring