Chapter 11: Practical Project Development

Haiyue
52min

Chapter 11: Practical Project Development

Learning Objectives

  1. Develop a file system management MCP Server
  2. Create a database operations MCP Server
  3. Implement an API gateway and proxy Server
  4. Build a content management and search Server
  5. Develop custom business logic Servers

11.1 Project One: File System Management Server

11.1.1 Project Requirements Analysis

// File System MCP Server functional requirements:
// 1. CRUD operations for files/directories
// 2. File content reading and writing
// 3. File searching and filtering
// 4. File permission management
// 5. File monitoring and change notifications
// 6. File metadata management

// src/projects/filesystem/FileSystemServer.ts
import path from 'path';
import fs from 'fs/promises';
import chokidar from 'chokidar';

export class FileSystemMCPServer extends SecureMCPServer {
  private watchers = new Map<string, chokidar.FSWatcher>();
  private allowedPaths: string[] = [];

  constructor(config: ServerConfig, allowedPaths: string[]) {
    super(config);
    this.allowedPaths = allowedPaths.map(p => path.resolve(p));
    this.setupFileSystemTools();
    this.setupFileSystemResources();
  }

  private setupFileSystemTools(): void {
    // File read tool
    this.addTool({
      name: 'fs_read_file',
      description: 'Read file content',
      inputSchema: {
        type: 'object',
        properties: {
          path: { type: 'string', description: 'File path' },
          encoding: { type: 'string', enum: ['utf8', 'base64'], default: 'utf8' }
        },
        required: ['path']
      }
    }, this.handleReadFile.bind(this));

    // File write tool
    this.addTool({
      name: 'fs_write_file',
      description: 'Write file content',
      inputSchema: {
        type: 'object',
        properties: {
          path: { type: 'string', description: 'File path' },
          content: { type: 'string', description: 'File content' },
          encoding: { type: 'string', enum: ['utf8', 'base64'], default: 'utf8' },
          createDir: { type: 'boolean', default: false, description: 'Whether to create directory' }
        },
        required: ['path', 'content']
      }
    }, this.handleWriteFile.bind(this));

    // Directory listing tool
    this.addTool({
      name: 'fs_list_directory',
      description: 'List directory contents',
      inputSchema: {
        type: 'object',
        properties: {
          path: { type: 'string', description: 'Directory path' },
          recursive: { type: 'boolean', default: false },
          includeStats: { type: 'boolean', default: false }
        },
        required: ['path']
      }
    }, this.handleListDirectory.bind(this));

    // File search tool
    this.addTool({
      name: 'fs_search_files',
      description: 'Search files',
      inputSchema: {
        type: 'object',
        properties: {
          path: { type: 'string', description: 'Search path' },
          pattern: { type: 'string', description: 'Search pattern (glob)' },
          content: { type: 'string', description: 'Content search' },
          maxResults: { type: 'number', default: 100 }
        },
        required: ['path']
      }
    }, this.handleSearchFiles.bind(this));

    // File operation tool
    this.addTool({
      name: 'fs_file_operation',
      description: 'File operations (copy, move, delete)',
      inputSchema: {
        type: 'object',
        properties: {
          operation: { type: 'string', enum: ['copy', 'move', 'delete', 'mkdir'] },
          source: { type: 'string', description: 'Source path' },
          target: { type: 'string', description: 'Target path' }
        },
        required: ['operation', 'source']
      }
    }, this.handleFileOperation.bind(this));
  }

  private setupFileSystemResources(): void {
    // File content resource
    this.addResource('file', async (uri: string) => {
      const filePath = this.parseFileUri(uri);
      this.validatePath(filePath);

      const content = await fs.readFile(filePath, 'utf8');
      const stats = await fs.stat(filePath);

      return {
        uri,
        mimeType: this.getMimeType(filePath),
        text: content,
        metadata: {
          size: stats.size,
          modified: stats.mtime,
          created: stats.ctime,
          type: stats.isDirectory() ? 'directory' : 'file'
        }
      };
    });

    // Directory resource
    this.addResource('directory', async (uri: string) => {
      const dirPath = this.parseFileUri(uri);
      this.validatePath(dirPath);

      const entries = await fs.readdir(dirPath, { withFileTypes: true });
      const items = entries.map(entry => ({
        name: entry.name,
        type: entry.isDirectory() ? 'directory' : 'file',
        path: path.join(dirPath, entry.name)
      }));

      return {
        uri,
        mimeType: 'application/json',
        text: JSON.stringify(items, null, 2),
        metadata: {
          type: 'directory',
          itemCount: items.length
        }
      };
    });
  }

  // Tool implementation methods
  private async handleReadFile(args: any): Promise<any> {
    const { path: filePath, encoding = 'utf8' } = args;
    this.validatePath(filePath);

    try {
      const content = await fs.readFile(filePath, encoding);
      const stats = await fs.stat(filePath);

      return {
        content,
        metadata: {
          size: stats.size,
          modified: stats.mtime,
          encoding
        }
      };
    } catch (error) {
      throw new ToolExecutionException(
        'fs_read_file',
        `Failed to read file: ${error.message}`,
        { filePath }
      );
    }
  }

  private async handleWriteFile(args: any): Promise<any> {
    const { path: filePath, content, encoding = 'utf8', createDir = false } = args;
    this.validatePath(filePath);

    try {
      if (createDir) {
        await fs.mkdir(path.dirname(filePath), { recursive: true });
      }

      await fs.writeFile(filePath, content, encoding);
      const stats = await fs.stat(filePath);

      return {
        success: true,
        metadata: {
          size: stats.size,
          modified: stats.mtime
        }
      };
    } catch (error) {
      throw new ToolExecutionException(
        'fs_write_file',
        `Failed to write file: ${error.message}`,
        { filePath }
      );
    }
  }

  private async handleListDirectory(args: any): Promise<any> {
    const { path: dirPath, recursive = false, includeStats = false } = args;
    this.validatePath(dirPath);

    try {
      const result = await this.listDirectoryRecursive(dirPath, recursive, includeStats);
      return { items: result };
    } catch (error) {
      throw new ToolExecutionException(
        'fs_list_directory',
        `Failed to list directory: ${error.message}`,
        { dirPath }
      );
    }
  }

  private async handleSearchFiles(args: any): Promise<any> {
    const { path: searchPath, pattern, content, maxResults = 100 } = args;
    this.validatePath(searchPath);

    try {
      const results = await this.searchFiles(searchPath, pattern, content, maxResults);
      return { results };
    } catch (error) {
      throw new ToolExecutionException(
        'fs_search_files',
        `Failed to search files: ${error.message}`,
        { searchPath, pattern }
      );
    }
  }

  private async handleFileOperation(args: any): Promise<any> {
    const { operation, source, target } = args;
    this.validatePath(source);
    if (target) this.validatePath(target);

    try {
      switch (operation) {
        case 'copy':
          await fs.copyFile(source, target!);
          break;
        case 'move':
          await fs.rename(source, target!);
          break;
        case 'delete':
          const stats = await fs.stat(source);
          if (stats.isDirectory()) {
            await fs.rmdir(source, { recursive: true });
          } else {
            await fs.unlink(source);
          }
          break;
        case 'mkdir':
          await fs.mkdir(source, { recursive: true });
          break;
        default:
          throw new Error(`Unknown operation: ${operation}`);
      }

      return { success: true, operation, source, target };
    } catch (error) {
      throw new ToolExecutionException(
        'fs_file_operation',
        `Failed to perform operation ${operation}: ${error.message}`,
        { operation, source, target }
      );
    }
  }

  // Helper methods
  private validatePath(filePath: string): void {
    const resolvedPath = path.resolve(filePath);
    const allowed = this.allowedPaths.some(allowedPath =>
      resolvedPath.startsWith(allowedPath)
    );

    if (!allowed) {
      throw new Error(`Access denied: Path ${filePath} is not in allowed paths`);
    }
  }

  private parseFileUri(uri: string): string {
    if (uri.startsWith('file://')) {
      return uri.substring(7);
    }
    return uri;
  }

  private getMimeType(filePath: string): string {
    const ext = path.extname(filePath).toLowerCase();
    const mimeTypes: Record<string, string> = {
      '.txt': 'text/plain',
      '.js': 'application/javascript',
      '.ts': 'application/typescript',
      '.json': 'application/json',
      '.html': 'text/html',
      '.css': 'text/css',
      '.md': 'text/markdown',
      '.py': 'text/x-python',
      '.java': 'text/x-java-source',
    };

    return mimeTypes[ext] || 'application/octet-stream';
  }

  private async listDirectoryRecursive(
    dirPath: string,
    recursive: boolean,
    includeStats: boolean
  ): Promise<any[]> {
    const entries = await fs.readdir(dirPath, { withFileTypes: true });
    const result: any[] = [];

    for (const entry of entries) {
      const fullPath = path.join(dirPath, entry.name);
      const item: any = {
        name: entry.name,
        path: fullPath,
        type: entry.isDirectory() ? 'directory' : 'file'
      };

      if (includeStats) {
        const stats = await fs.stat(fullPath);
        item.stats = {
          size: stats.size,
          modified: stats.mtime,
          created: stats.ctime,
          mode: stats.mode
        };
      }

      result.push(item);

      if (recursive && entry.isDirectory()) {
        const children = await this.listDirectoryRecursive(fullPath, true, includeStats);
        result.push(...children);
      }
    }

    return result;
  }

  private async searchFiles(
    searchPath: string,
    pattern?: string,
    content?: string,
    maxResults: number = 100
  ): Promise<any[]> {
    const glob = require('glob');
    const results: any[] = [];

    // Search files using glob pattern
    if (pattern) {
      const globPattern = path.join(searchPath, pattern);
      const files = glob.sync(globPattern, { nodir: true });

      for (const file of files.slice(0, maxResults)) {
        const stats = await fs.stat(file);
        const item = {
          path: file,
          name: path.basename(file),
          size: stats.size,
          modified: stats.mtime,
          type: 'file'
        };

        // If content search is required
        if (content) {
          try {
            const fileContent = await fs.readFile(file, 'utf8');
            if (fileContent.includes(content)) {
              item['matches'] = this.findContentMatches(fileContent, content);
              results.push(item);
            }
          } catch (error) {
            // Ignore files that cannot be read
          }
        } else {
          results.push(item);
        }

        if (results.length >= maxResults) break;
      }
    }

    return results;
  }

  private findContentMatches(content: string, searchTerm: string): any[] {
    const lines = content.split('\n');
    const matches: any[] = [];

    lines.forEach((line, index) => {
      if (line.includes(searchTerm)) {
        matches.push({
          line: index + 1,
          content: line.trim(),
          position: line.indexOf(searchTerm)
        });
      }
    });

    return matches.slice(0, 10); // Limit match results
  }
}

11.2 Project Two: Database Operations Server

11.2.1 Database MCP Server Implementation

// src/projects/database/DatabaseServer.ts
import { Pool } from 'pg'; // PostgreSQL
import mysql from 'mysql2/promise'; // MySQL
import sqlite3 from 'sqlite3';
import { Database } from 'sqlite3';

export interface DatabaseConfig {
  type: 'postgresql' | 'mysql' | 'sqlite';
  connection: {
    host?: string;
    port?: number;
    database: string;
    username?: string;
    password?: string;
    filename?: string; // for SQLite
  };
  pool?: {
    min: number;
    max: number;
  };
}

export class DatabaseMCPServer extends SecureMCPServer {
  private dbConfig: DatabaseConfig;
  private connection: any;
  private queryCache: Map<string, { result: any; timestamp: number }> = new Map();
  private cacheTimeout = 5 * 60 * 1000; // 5 minutes

  constructor(config: ServerConfig, dbConfig: DatabaseConfig) {
    super(config);
    this.dbConfig = dbConfig;
    this.setupDatabaseTools();
    this.setupDatabaseResources();
  }

  async start(): Promise<void> {
    await super.start();
    await this.connectToDatabase();
  }

  async stop(reason?: string): Promise<void> {
    await this.disconnectFromDatabase();
    await super.stop(reason);
  }

  private async connectToDatabase(): Promise<void> {
    try {
      switch (this.dbConfig.type) {
        case 'postgresql':
          this.connection = new Pool({
            host: this.dbConfig.connection.host,
            port: this.dbConfig.connection.port,
            database: this.dbConfig.connection.database,
            user: this.dbConfig.connection.username,
            password: this.dbConfig.connection.password,
            min: this.dbConfig.pool?.min || 1,
            max: this.dbConfig.pool?.max || 10,
          });
          break;

        case 'mysql':
          this.connection = mysql.createPool({
            host: this.dbConfig.connection.host,
            port: this.dbConfig.connection.port,
            database: this.dbConfig.connection.database,
            user: this.dbConfig.connection.username,
            password: this.dbConfig.connection.password,
            connectionLimit: this.dbConfig.pool?.max || 10,
          });
          break;

        case 'sqlite':
          this.connection = new sqlite3.Database(
            this.dbConfig.connection.filename!,
            sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE
          );
          break;

        default:
          throw new Error(`Unsupported database type: ${this.dbConfig.type}`);
      }

      this.logger.info('Database connected successfully', { type: this.dbConfig.type });
    } catch (error) {
      this.logger.error('Database connection failed', error);
      throw error;
    }
  }

  private async disconnectFromDatabase(): Promise<void> {
    if (this.connection) {
      try {
        switch (this.dbConfig.type) {
          case 'postgresql':
          case 'mysql':
            await this.connection.end();
            break;
          case 'sqlite':
            this.connection.close();
            break;
        }
        this.logger.info('Database disconnected');
      } catch (error) {
        this.logger.error('Database disconnection error', error);
      }
    }
  }

  private setupDatabaseTools(): void {
    // Query tool
    this.addTool({
      name: 'db_query',
      description: 'Execute SQL query',
      inputSchema: {
        type: 'object',
        properties: {
          sql: { type: 'string', description: 'SQL query statement' },
          params: { type: 'array', description: 'Query parameters', items: { type: 'string' } },
          useCache: { type: 'boolean', default: true, description: 'Whether to use cache' },
          limit: { type: 'number', default: 100, description: 'Result limit' }
        },
        required: ['sql']
      }
    }, this.handleQuery.bind(this));

    // Execute tool (INSERT, UPDATE, DELETE)
    this.addTool({
      name: 'db_execute',
      description: 'Execute SQL command',
      inputSchema: {
        type: 'object',
        properties: {
          sql: { type: 'string', description: 'SQL command' },
          params: { type: 'array', description: 'Command parameters', items: { type: 'string' } },
          transaction: { type: 'boolean', default: false, description: 'Whether to use transaction' }
        },
        required: ['sql']
      }
    }, this.handleExecute.bind(this));

    // Table info tool
    this.addTool({
      name: 'db_table_info',
      description: 'Get table information',
      inputSchema: {
        type: 'object',
        properties: {
          table: { type: 'string', description: 'Table name' },
          includeColumns: { type: 'boolean', default: true },
          includeIndexes: { type: 'boolean', default: false }
        },
        required: ['table']
      }
    }, this.handleTableInfo.bind(this));

    // Database schema tool
    this.addTool({
      name: 'db_schema',
      description: 'Get database schema',
      inputSchema: {
        type: 'object',
        properties: {
          includeViews: { type: 'boolean', default: false },
          includeIndexes: { type: 'boolean', default: false }
        }
      }
    }, this.handleSchema.bind(this));
  }

  private setupDatabaseResources(): void {
    // Table data resource
    this.addResource('table', async (uri: string) => {
      const tableName = this.parseTableUri(uri);
      const sql = this.buildSelectQuery(tableName);
      const result = await this.executeQuery(sql);

      return {
        uri,
        mimeType: 'application/json',
        text: JSON.stringify(result.rows, null, 2),
        metadata: {
          table: tableName,
          rowCount: result.rowCount,
          columns: result.columns
        }
      };
    });

    // Query result resource
    this.addResource('query', async (uri: string) => {
      const queryId = this.parseQueryUri(uri);
      const cachedResult = this.queryCache.get(queryId);

      if (!cachedResult) {
        throw new Error(`Query result not found: ${queryId}`);
      }

      return {
        uri,
        mimeType: 'application/json',
        text: JSON.stringify(cachedResult.result, null, 2),
        metadata: {
          cached: true,
          timestamp: cachedResult.timestamp
        }
      };
    });
  }

  // Tool implementation methods
  private async handleQuery(args: any): Promise<any> {
    const { sql, params = [], useCache = true, limit = 100 } = args;

    try {
      // Check cache
      if (useCache) {
        const cacheKey = this.generateCacheKey(sql, params);
        const cached = this.queryCache.get(cacheKey);

        if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
          this.logger.debug('Query cache hit', { sql: sql.substring(0, 100) });
          return { ...cached.result, fromCache: true };
        }
      }

      // Add LIMIT clause (if not exists)
      const limitedSql = this.addLimitClause(sql, limit);

      const result = await this.executeQuery(limitedSql, params);

      // Cache result
      if (useCache) {
        const cacheKey = this.generateCacheKey(sql, params);
        this.queryCache.set(cacheKey, {
          result,
          timestamp: Date.now()
        });

        // Clean expired cache
        this.cleanExpiredCache();
      }

      return result;
    } catch (error) {
      throw new ToolExecutionException(
        'db_query',
        `Query execution failed: ${error.message}`,
        { sql: sql.substring(0, 200), params }
      );
    }
  }

  private async handleExecute(args: any): Promise<any> {
    const { sql, params = [], transaction = false } = args;

    try {
      let result;

      if (transaction) {
        result = await this.executeTransaction(async (client) => {
          return await this.executeCommand(sql, params, client);
        });
      } else {
        result = await this.executeCommand(sql, params);
      }

      // Clear related cache
      this.invalidateQueryCache();

      return result;
    } catch (error) {
      throw new ToolExecutionException(
        'db_execute',
        `Command execution failed: ${error.message}`,
        { sql: sql.substring(0, 200), params }
      );
    }
  }

  private async handleTableInfo(args: any): Promise<any> {
    const { table, includeColumns = true, includeIndexes = false } = args;

    try {
      const info: any = { name: table };

      if (includeColumns) {
        info.columns = await this.getTableColumns(table);
      }

      if (includeIndexes) {
        info.indexes = await this.getTableIndexes(table);
      }

      info.rowCount = await this.getTableRowCount(table);

      return info;
    } catch (error) {
      throw new ToolExecutionException(
        'db_table_info',
        `Failed to get table info: ${error.message}`,
        { table }
      );
    }
  }

  private async handleSchema(args: any): Promise<any> {
    const { includeViews = false, includeIndexes = false } = args;

    try {
      const schema: any = {
        tables: await this.getAllTables()
      };

      if (includeViews) {
        schema.views = await this.getAllViews();
      }

      if (includeIndexes) {
        schema.indexes = await this.getAllIndexes();
      }

      return schema;
    } catch (error) {
      throw new ToolExecutionException(
        'db_schema',
        `Failed to get schema: ${error.message}`,
        args
      );
    }
  }

  // Database operation methods
  private async executeQuery(sql: string, params: any[] = []): Promise<any> {
    switch (this.dbConfig.type) {
      case 'postgresql':
        const pgResult = await this.connection.query(sql, params);
        return {
          rows: pgResult.rows,
          rowCount: pgResult.rowCount,
          columns: pgResult.fields?.map((f: any) => f.name) || []
        };

      case 'mysql':
        const [mysqlRows, mysqlFields] = await this.connection.execute(sql, params);
        return {
          rows: mysqlRows,
          rowCount: Array.isArray(mysqlRows) ? mysqlRows.length : 0,
          columns: mysqlFields?.map((f: any) => f.name) || []
        };

      case 'sqlite':
        return new Promise((resolve, reject) => {
          const stmt = this.connection.prepare(sql);
          stmt.all(params, (err: any, rows: any[]) => {
            if (err) {
              reject(err);
            } else {
              resolve({
                rows,
                rowCount: rows.length,
                columns: rows.length > 0 ? Object.keys(rows[0]) : []
              });
            }
          });
        });

      default:
        throw new Error(`Unsupported database type: ${this.dbConfig.type}`);
    }
  }

  private async executeCommand(sql: string, params: any[] = [], client?: any): Promise<any> {
    const connection = client || this.connection;

    switch (this.dbConfig.type) {
      case 'postgresql':
        const pgResult = await connection.query(sql, params);
        return {
          success: true,
          affectedRows: pgResult.rowCount,
          insertId: pgResult.insertId
        };

      case 'mysql':
        const [mysqlResult] = await connection.execute(sql, params);
        return {
          success: true,
          affectedRows: (mysqlResult as any).affectedRows,
          insertId: (mysqlResult as any).insertId
        };

      case 'sqlite':
        return new Promise((resolve, reject) => {
          this.connection.run(sql, params, function(err: any) {
            if (err) {
              reject(err);
            } else {
              resolve({
                success: true,
                affectedRows: this.changes,
                insertId: this.lastID
              });
            }
          });
        });

      default:
        throw new Error(`Unsupported database type: ${this.dbConfig.type}`);
    }
  }

  private async executeTransaction(callback: (client: any) => Promise<any>): Promise<any> {
    switch (this.dbConfig.type) {
      case 'postgresql':
        const client = await this.connection.connect();
        try {
          await client.query('BEGIN');
          const result = await callback(client);
          await client.query('COMMIT');
          return result;
        } catch (error) {
          await client.query('ROLLBACK');
          throw error;
        } finally {
          client.release();
        }

      case 'mysql':
        const connection = await this.connection.getConnection();
        try {
          await connection.beginTransaction();
          const result = await callback(connection);
          await connection.commit();
          return result;
        } catch (error) {
          await connection.rollback();
          throw error;
        } finally {
          connection.release();
        }

      case 'sqlite':
        // SQLite transaction handling
        return new Promise((resolve, reject) => {
          this.connection.serialize(() => {
            this.connection.run('BEGIN TRANSACTION');

            callback(this.connection)
              .then(result => {
                this.connection.run('COMMIT', (err: any) => {
                  if (err) reject(err);
                  else resolve(result);
                });
              })
              .catch(error => {
                this.connection.run('ROLLBACK', () => {
                  reject(error);
                });
              });
          });
        });

      default:
        throw new Error(`Unsupported database type: ${this.dbConfig.type}`);
    }
  }

  // Helper methods
  private parseTableUri(uri: string): string {
    return uri.replace('table://', '');
  }

  private parseQueryUri(uri: string): string {
    return uri.replace('query://', '');
  }

  private buildSelectQuery(tableName: string): string {
    return `SELECT * FROM ${tableName} LIMIT 100`;
  }

  private addLimitClause(sql: string, limit: number): string {
    if (sql.toLowerCase().includes('limit')) {
      return sql;
    }
    return `${sql} LIMIT ${limit}`;
  }

  private generateCacheKey(sql: string, params: any[]): string {
    const crypto = require('crypto');
    const content = sql + JSON.stringify(params);
    return crypto.createHash('sha256').update(content).digest('hex');
  }

  private cleanExpiredCache(): void {
    const now = Date.now();
    for (const [key, value] of this.queryCache.entries()) {
      if (now - value.timestamp > this.cacheTimeout) {
        this.queryCache.delete(key);
      }
    }
  }

  private invalidateQueryCache(): void {
    this.queryCache.clear();
    this.logger.debug('Query cache invalidated');
  }

  private async getAllTables(): Promise<string[]> {
    let sql: string;

    switch (this.dbConfig.type) {
      case 'postgresql':
        sql = `SELECT tablename FROM pg_tables WHERE schemaname = 'public'`;
        break;
      case 'mysql':
        sql = `SHOW TABLES`;
        break;
      case 'sqlite':
        sql = `SELECT name FROM sqlite_master WHERE type='table'`;
        break;
      default:
        throw new Error(`Unsupported database type: ${this.dbConfig.type}`);
    }

    const result = await this.executeQuery(sql);
    return result.rows.map((row: any) => Object.values(row)[0]);
  }

  private async getTableColumns(tableName: string): Promise<any[]> {
    let sql: string;

    switch (this.dbConfig.type) {
      case 'postgresql':
        sql = `SELECT column_name, data_type, is_nullable
               FROM information_schema.columns
               WHERE table_name = $1`;
        return (await this.executeQuery(sql, [tableName])).rows;

      case 'mysql':
        sql = `DESCRIBE ${tableName}`;
        return (await this.executeQuery(sql)).rows;

      case 'sqlite':
        sql = `PRAGMA table_info(${tableName})`;
        return (await this.executeQuery(sql)).rows;

      default:
        throw new Error(`Unsupported database type: ${this.dbConfig.type}`);
    }
  }

  private async getTableRowCount(tableName: string): Promise<number> {
    const sql = `SELECT COUNT(*) as count FROM ${tableName}`;
    const result = await this.executeQuery(sql);
    return result.rows[0].count;
  }

  private async getTableIndexes(tableName: string): Promise<any[]> {
    // Implement logic to get table indexes
    return [];
  }

  private async getAllViews(): Promise<string[]> {
    // Implement logic to get all views
    return [];
  }

  private async getAllIndexes(): Promise<any[]> {
    // Implement logic to get all indexes
    return [];
  }
}

11.3 Project Three: API Gateway and Proxy Server

11.3.1 API Gateway Implementation

// src/projects/gateway/ApiGatewayServer.ts
export interface ServiceConfig {
  name: string;
  baseUrl: string;
  timeout: number;
  retries: number;
  circuitBreaker: {
    enabled: boolean;
    threshold: number;
    timeout: number;
  };
}

export interface RouteConfig {
  path: string;
  method: string;
  service: string;
  targetPath?: string;
  auth: boolean;
  rateLimit?: {
    requests: number;
    window: number;
  };
}

export class ApiGatewayMCPServer extends SecureMCPServer {
  private services = new Map<string, ServiceConfig>();
  private routes = new Map<string, RouteConfig>();
  private circuitBreakers = new Map<string, CircuitBreaker>();

  constructor(config: ServerConfig) {
    super(config);
    this.setupGatewayTools();
    this.setupGatewayResources();
  }

  private setupGatewayTools(): void {
    // API proxy tool
    this.addTool({
      name: 'gateway_proxy',
      description: 'API proxy request',
      inputSchema: {
        type: 'object',
        properties: {
          path: { type: 'string', description: 'Request path' },
          method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'DELETE'] },
          headers: { type: 'object', description: 'Request headers' },
          body: { type: 'string', description: 'Request body' },
          params: { type: 'object', description: 'Query parameters' }
        },
        required: ['path', 'method']
      }
    }, this.handleProxy.bind(this));

    // Service registration tool
    this.addTool({
      name: 'gateway_register_service',
      description: 'Register service',
      inputSchema: {
        type: 'object',
        properties: {
          name: { type: 'string' },
          baseUrl: { type: 'string' },
          timeout: { type: 'number', default: 30000 },
          retries: { type: 'number', default: 3 }
        },
        required: ['name', 'baseUrl']
      }
    }, this.handleRegisterService.bind(this));

    // Route configuration tool
    this.addTool({
      name: 'gateway_add_route',
      description: 'Add route',
      inputSchema: {
        type: 'object',
        properties: {
          path: { type: 'string' },
          method: { type: 'string' },
          service: { type: 'string' },
          targetPath: { type: 'string' },
          auth: { type: 'boolean', default: true }
        },
        required: ['path', 'method', 'service']
      }
    }, this.handleAddRoute.bind(this));
  }

  private setupGatewayResources(): void {
    // Service status resource
    this.addResource('services', async () => {
      const services = Array.from(this.services.entries()).map(([name, config]) => ({
        name,
        ...config,
        status: this.getServiceStatus(name)
      }));

      return {
        uri: 'services://',
        mimeType: 'application/json',
        text: JSON.stringify(services, null, 2)
      };
    });

    // Route configuration resource
    this.addResource('routes', async () => {
      const routes = Array.from(this.routes.values());

      return {
        uri: 'routes://',
        mimeType: 'application/json',
        text: JSON.stringify(routes, null, 2)
      };
    });
  }

  private async handleProxy(args: any): Promise<any> {
    const { path, method, headers = {}, body, params = {} } = args;

    // Find matching route
    const route = this.findMatchingRoute(path, method);
    if (!route) {
      throw new Error(`No route found for ${method} ${path}`);
    }

    // Get service configuration
    const service = this.services.get(route.service);
    if (!service) {
      throw new Error(`Service not found: ${route.service}`);
    }

    // Check circuit breaker
    const circuitBreaker = this.getCircuitBreaker(route.service);
    if (!circuitBreaker.allowRequest()) {
      throw new Error(`Circuit breaker open for service: ${route.service}`);
    }

    try {
      // Build target URL
      const targetPath = route.targetPath || path;
      const url = new URL(targetPath, service.baseUrl);

      // Add query parameters
      Object.entries(params).forEach(([key, value]) => {
        url.searchParams.set(key, String(value));
      });

      // Send request
      const response = await this.makeRequest(url.toString(), {
        method,
        headers,
        body: body ? JSON.stringify(body) : undefined,
        timeout: service.timeout,
      });

      circuitBreaker.recordSuccess();

      return {
        status: response.status,
        headers: response.headers,
        body: await response.text(),
        service: route.service,
        targetUrl: url.toString()
      };

    } catch (error) {
      circuitBreaker.recordFailure();
      throw new ToolExecutionException(
        'gateway_proxy',
        `Proxy request failed: ${error.message}`,
        { path, method, service: route.service }
      );
    }
  }

  private async handleRegisterService(args: any): Promise<any> {
    const { name, baseUrl, timeout = 30000, retries = 3 } = args;

    const serviceConfig: ServiceConfig = {
      name,
      baseUrl,
      timeout,
      retries,
      circuitBreaker: {
        enabled: true,
        threshold: 5,
        timeout: 60000
      }
    };

    this.services.set(name, serviceConfig);
    this.circuitBreakers.set(name, new CircuitBreaker(serviceConfig.circuitBreaker));

    this.logger.info('Service registered', { name, baseUrl });

    return { success: true, service: name };
  }

  private async handleAddRoute(args: any): Promise<any> {
    const { path, method, service, targetPath, auth = true } = args;

    // Verify service exists
    if (!this.services.has(service)) {
      throw new Error(`Service not found: ${service}`);
    }

    const routeKey = `${method.toUpperCase()}:${path}`;
    const routeConfig: RouteConfig = {
      path,
      method: method.toUpperCase(),
      service,
      targetPath,
      auth
    };

    this.routes.set(routeKey, routeConfig);

    this.logger.info('Route added', { path, method, service });

    return { success: true, route: routeKey };
  }

  private findMatchingRoute(path: string, method: string): RouteConfig | null {
    const exactKey = `${method.toUpperCase()}:${path}`;
    const exactMatch = this.routes.get(exactKey);

    if (exactMatch) {
      return exactMatch;
    }

    // Support path parameter matching (simplified version)
    for (const [key, route] of this.routes) {
      if (this.matchesRoute(route, path, method)) {
        return route;
      }
    }

    return null;
  }

  private matchesRoute(route: RouteConfig, path: string, method: string): boolean {
    if (route.method !== method.toUpperCase()) {
      return false;
    }

    // Simplified path matching logic
    const routePattern = route.path.replace(/:\w+/g, '([^/]+)');
    const regex = new RegExp(`^${routePattern}$`);
    return regex.test(path);
  }

  private getCircuitBreaker(serviceName: string): CircuitBreaker {
    let circuitBreaker = this.circuitBreakers.get(serviceName);

    if (!circuitBreaker) {
      const service = this.services.get(serviceName);
      circuitBreaker = new CircuitBreaker(service?.circuitBreaker || {
        enabled: true,
        threshold: 5,
        timeout: 60000
      });
      this.circuitBreakers.set(serviceName, circuitBreaker);
    }

    return circuitBreaker;
  }

  private async makeRequest(url: string, options: any): Promise<Response> {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), options.timeout || 30000);

    try {
      const response = await fetch(url, {
        ...options,
        signal: controller.signal
      });

      return response;
    } finally {
      clearTimeout(timeoutId);
    }
  }

  private getServiceStatus(serviceName: string): string {
    const circuitBreaker = this.circuitBreakers.get(serviceName);

    if (!circuitBreaker) {
      return 'unknown';
    }

    if (circuitBreaker.isOpen()) {
      return 'circuit_open';
    } else if (circuitBreaker.isHalfOpen()) {
      return 'half_open';
    } else {
      return 'healthy';
    }
  }
}

// Circuit breaker implementation
class CircuitBreaker {
  private failures = 0;
  private lastFailTime = 0;
  private state: 'closed' | 'open' | 'half_open' = 'closed';

  constructor(private config: { enabled: boolean; threshold: number; timeout: number }) {}

  allowRequest(): boolean {
    if (!this.config.enabled) {
      return true;
    }

    const now = Date.now();

    if (this.state === 'open') {
      if (now - this.lastFailTime > this.config.timeout) {
        this.state = 'half_open';
        return true;
      }
      return false;
    }

    return true;
  }

  recordSuccess(): void {
    this.failures = 0;
    this.state = 'closed';
  }

  recordFailure(): void {
    this.failures++;
    this.lastFailTime = Date.now();

    if (this.failures >= this.config.threshold) {
      this.state = 'open';
    }
  }

  isOpen(): boolean {
    return this.state === 'open';
  }

  isHalfOpen(): boolean {
    return this.state === 'half_open';
  }
}

11.4 Project Four: Content Management and Search Server

11.4.1 Content Management System Implementation

// src/projects/cms/ContentManagementServer.ts
export interface ContentItem {
  id: string;
  title: string;
  content: string;
  type: 'article' | 'page' | 'document' | 'media';
  status: 'draft' | 'published' | 'archived';
  author: string;
  tags: string[];
  metadata: Record<string, any>;
  createdAt: Date;
  updatedAt: Date;
}

export interface SearchIndex {
  id: string;
  title: string;
  content: string;
  tokens: string[];
  type: string;
  tags: string[];
  score?: number;
}

export class ContentManagementMCPServer extends SecureMCPServer {
  private contents = new Map<string, ContentItem>();
  private searchIndex = new Map<string, SearchIndex>();
  private contentStorage: string; // Content storage directory

  constructor(config: ServerConfig, contentStorage: string) {
    super(config);
    this.contentStorage = contentStorage;
    this.setupCMSTools();
    this.setupCMSResources();
  }

  async start(): Promise<void> {
    await super.start();
    await this.loadContentFromStorage();
    this.buildSearchIndex();
  }

  private setupCMSTools(): void {
    // Create content tool
    this.addTool({
      name: 'cms_create_content',
      description: 'Create content',
      inputSchema: {
        type: 'object',
        properties: {
          title: { type: 'string' },
          content: { type: 'string' },
          type: { type: 'string', enum: ['article', 'page', 'document', 'media'] },
          tags: { type: 'array', items: { type: 'string' } },
          metadata: { type: 'object' }
        },
        required: ['title', 'content', 'type']
      }
    }, this.handleCreateContent.bind(this));

    // Update content tool
    this.addTool({
      name: 'cms_update_content',
      description: 'Update content',
      inputSchema: {
        type: 'object',
        properties: {
          id: { type: 'string' },
          title: { type: 'string' },
          content: { type: 'string' },
          tags: { type: 'array', items: { type: 'string' } },
          status: { type: 'string', enum: ['draft', 'published', 'archived'] },
          metadata: { type: 'object' }
        },
        required: ['id']
      }
    }, this.handleUpdateContent.bind(this));

    // Delete content tool
    this.addTool({
      name: 'cms_delete_content',
      description: 'Delete content',
      inputSchema: {
        type: 'object',
        properties: {
          id: { type: 'string' }
        },
        required: ['id']
      }
    }, this.handleDeleteContent.bind(this));

    // Search content tool
    this.addTool({
      name: 'cms_search_content',
      description: 'Search content',
      inputSchema: {
        type: 'object',
        properties: {
          query: { type: 'string' },
          type: { type: 'string' },
          tags: { type: 'array', items: { type: 'string' } },
          status: { type: 'string' },
          limit: { type: 'number', default: 20 }
        },
        required: ['query']
      }
    }, this.handleSearchContent.bind(this));

    // Get content list tool
    this.addTool({
      name: 'cms_list_content',
      description: 'Get content list',
      inputSchema: {
        type: 'object',
        properties: {
          type: { type: 'string' },
          status: { type: 'string' },
          author: { type: 'string' },
          limit: { type: 'number', default: 20 },
          offset: { type: 'number', default: 0 }
        }
      }
    }, this.handleListContent.bind(this));
  }

  private setupCMSResources(): void {
    // Content resource
    this.addResource('content', async (uri: string) => {
      const contentId = this.parseContentUri(uri);
      const content = this.contents.get(contentId);

      if (!content) {
        throw new Error(`Content not found: ${contentId}`);
      }

      return {
        uri,
        mimeType: 'application/json',
        text: JSON.stringify(content, null, 2),
        metadata: {
          id: content.id,
          type: content.type,
          status: content.status,
          lastModified: content.updatedAt
        }
      };
    });

    // Content list resource
    this.addResource('content-list', async (uri: string) => {
      const params = this.parseListUri(uri);
      const contents = this.filterContents(params);

      return {
        uri,
        mimeType: 'application/json',
        text: JSON.stringify(contents, null, 2),
        metadata: {
          total: contents.length,
          filters: params
        }
      };
    });
  }

  // Tool implementation methods
  private async handleCreateContent(args: any): Promise<any> {
    const { title, content, type, tags = [], metadata = {} } = args;

    const contentItem: ContentItem = {
      id: this.generateContentId(),
      title,
      content,
      type,
      status: 'draft',
      author: 'system', // Should be obtained from authentication info in practice
      tags,
      metadata,
      createdAt: new Date(),
      updatedAt: new Date()
    };

    this.contents.set(contentItem.id, contentItem);
    await this.saveContentToStorage(contentItem);
    this.addToSearchIndex(contentItem);

    this.logger.info('Content created', { id: contentItem.id, title, type });

    return {
      success: true,
      id: contentItem.id,
      content: contentItem
    };
  }

  private async handleUpdateContent(args: any): Promise<any> {
    const { id, ...updates } = args;

    const existingContent = this.contents.get(id);
    if (!existingContent) {
      throw new Error(`Content not found: ${id}`);
    }

    const updatedContent: ContentItem = {
      ...existingContent,
      ...updates,
      updatedAt: new Date()
    };

    this.contents.set(id, updatedContent);
    await this.saveContentToStorage(updatedContent);
    this.updateSearchIndex(updatedContent);

    this.logger.info('Content updated', { id, updates: Object.keys(updates) });

    return {
      success: true,
      content: updatedContent
    };
  }

  private async handleDeleteContent(args: any): Promise<any> {
    const { id } = args;

    const content = this.contents.get(id);
    if (!content) {
      throw new Error(`Content not found: ${id}`);
    }

    this.contents.delete(id);
    await this.deleteContentFromStorage(id);
    this.removeFromSearchIndex(id);

    this.logger.info('Content deleted', { id, title: content.title });

    return {
      success: true,
      deletedContent: content
    };
  }

  private async handleSearchContent(args: any): Promise<any> {
    const { query, type, tags, status, limit = 20 } = args;

    const results = this.searchContents({
      query,
      type,
      tags,
      status,
      limit
    });

    return {
      results,
      query,
      total: results.length
    };
  }

  private async handleListContent(args: any): Promise<any> {
    const { type, status, author, limit = 20, offset = 0 } = args;

    const contents = this.filterContents({ type, status, author });
    const paginatedContents = contents.slice(offset, offset + limit);

    return {
      contents: paginatedContents,
      total: contents.length,
      offset,
      limit
    };
  }

  // Search-related methods
  private buildSearchIndex(): void {
    for (const content of this.contents.values()) {
      this.addToSearchIndex(content);
    }

    this.logger.info('Search index built', { totalItems: this.searchIndex.size });
  }

  private addToSearchIndex(content: ContentItem): void {
    const tokens = this.tokenize(content.title + ' ' + content.content);

    const indexItem: SearchIndex = {
      id: content.id,
      title: content.title,
      content: content.content.substring(0, 500), // Summary
      tokens,
      type: content.type,
      tags: content.tags
    };

    this.searchIndex.set(content.id, indexItem);
  }

  private updateSearchIndex(content: ContentItem): void {
    this.addToSearchIndex(content);
  }

  private removeFromSearchIndex(contentId: string): void {
    this.searchIndex.delete(contentId);
  }

  private searchContents(criteria: any): ContentItem[] {
    const { query, type, tags, status, limit } = criteria;

    let candidates = Array.from(this.contents.values());

    // Basic filtering
    if (type) {
      candidates = candidates.filter(c => c.type === type);
    }

    if (status) {
      candidates = candidates.filter(c => c.status === status);
    }

    if (tags && tags.length > 0) {
      candidates = candidates.filter(c =>
        tags.some((tag: string) => c.tags.includes(tag))
      );
    }

    // Text search
    if (query) {
      const queryTokens = this.tokenize(query.toLowerCase());
      const searchResults = this.performTextSearch(queryTokens, candidates);
      candidates = searchResults.sort((a, b) => (b.score || 0) - (a.score || 0));
    }

    return candidates.slice(0, limit);
  }

  private performTextSearch(queryTokens: string[], candidates: ContentItem[]): any[] {
    return candidates.map(content => {
      const indexItem = this.searchIndex.get(content.id);
      if (!indexItem) {
        return { ...content, score: 0 };
      }

      let score = 0;

      // Title matching has higher weight
      const titleTokens = this.tokenize(content.title.toLowerCase());
      for (const token of queryTokens) {
        if (titleTokens.includes(token)) {
          score += 10;
        }
        if (indexItem.tokens.includes(token)) {
          score += 1;
        }
      }

      // Tag matching
      for (const token of queryTokens) {
        if (content.tags.some(tag => tag.toLowerCase().includes(token))) {
          score += 5;
        }
      }

      return { ...content, score };
    }).filter(item => item.score > 0);
  }

  private tokenize(text: string): string[] {
    return text
      .toLowerCase()
      .replace(/[^\w\s]/g, ' ')
      .split(/\s+/)
      .filter(token => token.length > 2);
  }

  // Storage-related methods
  private async loadContentFromStorage(): Promise<void> {
    try {
      const contentDir = path.join(this.contentStorage, 'contents');
      const files = await fs.readdir(contentDir);

      for (const file of files) {
        if (file.endsWith('.json')) {
          const filePath = path.join(contentDir, file);
          const data = await fs.readFile(filePath, 'utf8');
          const content: ContentItem = JSON.parse(data);
          this.contents.set(content.id, content);
        }
      }

      this.logger.info('Contents loaded from storage', { count: this.contents.size });
    } catch (error) {
      this.logger.warn('Failed to load contents from storage', { error });
    }
  }

  private async saveContentToStorage(content: ContentItem): Promise<void> {
    try {
      const contentDir = path.join(this.contentStorage, 'contents');
      await fs.mkdir(contentDir, { recursive: true });

      const filePath = path.join(contentDir, `${content.id}.json`);
      await fs.writeFile(filePath, JSON.stringify(content, null, 2));
    } catch (error) {
      this.logger.error('Failed to save content to storage', { error, contentId: content.id });
    }
  }

  private async deleteContentFromStorage(contentId: string): Promise<void> {
    try {
      const filePath = path.join(this.contentStorage, 'contents', `${contentId}.json`);
      await fs.unlink(filePath);
    } catch (error) {
      this.logger.warn('Failed to delete content from storage', { error, contentId });
    }
  }

  // Helper methods
  private generateContentId(): string {
    return `content_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }

  private parseContentUri(uri: string): string {
    return uri.replace('content://', '');
  }

  private parseListUri(uri: string): any {
    const params = new URLSearchParams(uri.replace('content-list://', ''));
    return Object.fromEntries(params.entries());
  }

  private filterContents(criteria: any): ContentItem[] {
    const { type, status, author } = criteria;

    return Array.from(this.contents.values()).filter(content => {
      if (type && content.type !== type) return false;
      if (status && content.status !== status) return false;
      if (author && content.author !== author) return false;
      return true;
    });
  }
}

Chapter Summary

Chapter 11 demonstrates the application of MCP Server in different domains through four complete practical projects:

Practical Project Summary

  1. File System Management Server: Implemented complete file operations, search, and monitoring functionality
  2. Database Operations Server: Supports multiple databases with query, transaction, and caching mechanisms
  3. API Gateway Proxy Server: Provides service registration, route configuration, and circuit breaker protection
  4. Content Management Search Server: Built a content management and full-text search system

Core Practice Points

  • Properly design tool and resource interfaces
  • Implement comprehensive error handling and logging
  • Provide flexible configuration and extension mechanisms
  • Focus on security and permission control
  • Optimize performance and user experience

Through learning these practical projects, you’ve mastered the development techniques and best practices of MCP Server in real business scenarios, laying a solid foundation for developing complex production-level applications.