Skip to content

Build MCP Server đầu tiên

Overview

Tutorial này sẽ hướng dẫn bạn build một MCP server đơn giản từ đầu. Chúng ta sẽ tạo một File System MCP Server cho phép AI agents read/write files và manage directories.

Prerequisites

Requirements

bash
# Node.js >= 20.19.0
node --version

# npm hoặc yarn
npm --version

# Basic JavaScript knowledge
# Understanding of JSON-RPC

Project Setup

bash
# Create project directory
mkdir my-mcp-server
cd my-mcp-server

# Initialize npm project
npm init -y

# Install dependencies
npm install @modelcontextprotocol/sdk

# Install development dependencies
npm install --save-dev nodemon @types/node typescript

Project Structure

my-mcp-server/
├── src/
│   ├── index.ts          # Main server file
│   ├── tools/            # Tool implementations
│   │   ├── file.ts      # File operations
│   │   └── directory.ts # Directory operations
│   ├── resources/        # Resource implementations
│   │   └── file.ts      # File resources
│   └── utils/            # Utility functions
│       └── validation.ts # Input validation
├── package.json
├── tsconfig.json
└── README.md

Step 1: Basic Server Structure

TypeScript Configuration

json
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Main Server File

typescript
// src/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ErrorCode,
  ListToolsRequestSchema,
  McpError,
  ReadResourceRequestSchema,
  ListResourcesRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

import { FileTools } from './tools/file.js';
import { DirectoryTools } from './tools/directory.js';
import { FileResources } from './resources/file.js';

class FileSystemMCPServer {
  private server: Server;
  private fileTools: FileTools;
  private directoryTools: DirectoryTools;
  private fileResources: FileResources;

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

    this.fileTools = new FileTools();
    this.directoryTools = new DirectoryTools();
    this.fileResources = new FileResources();

    this.setupHandlers();
  }

  private setupHandlers() {
    // Tool handlers
    this.server.setRequestHandler(ListToolsRequestSchema, async () => {
      const fileTools = await this.fileTools.listTools();
      const directoryTools = await this.directoryTools.listTools();
      
      return {
        tools: [...fileTools.tools, ...directoryTools.tools],
      };
    });

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

      try {
        if (name.startsWith('file_')) {
          return await this.fileTools.callTool(name, args);
        } else if (name.startsWith('directory_')) {
          return await this.directoryTools.callTool(name, args);
        } else {
          throw new McpError(
            ErrorCode.MethodNotFound,
            `Unknown tool: ${name}`
          );
        }
      } catch (error) {
        if (error instanceof McpError) {
          throw error;
        }
        
        throw new McpError(
          ErrorCode.InternalError,
          `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`
        );
      }
    });

    // Resource handlers
    this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
      return await this.fileResources.listResources();
    });

    this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
      return await this.fileResources.readResource(request.params.uri);
    });
  }

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

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

Step 2: Implement Tools

File Tools

typescript
// src/tools/file.ts
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import * as fs from 'fs/promises';
import * as path from 'path';
import { validatePath } from '../utils/validation.js';

export class FileTools {
  private basePath: string;

  constructor(basePath: string = process.cwd()) {
    this.basePath = path.resolve(basePath);
  }

  async listTools(): Promise<{ tools: Tool[] }> {
    return {
      tools: [
        {
          name: 'file_read',
          description: 'Read contents of a file',
          inputSchema: {
            type: 'object',
            properties: {
              path: {
                type: 'string',
                description: 'Path to the file to read'
              }
            },
            required: ['path']
          }
        },
        {
          name: 'file_write',
          description: 'Write content to a file',
          inputSchema: {
            type: 'object',
            properties: {
              path: {
                type: 'string',
                description: 'Path to the file to write'
              },
              content: {
                type: 'string',
                description: 'Content to write to the file'
              },
              createDir: {
                type: 'boolean',
                description: 'Create parent directories if they don\'t exist',
                default: false
              }
            },
            required: ['path', 'content']
          }
        },
        {
          name: 'file_delete',
          description: 'Delete a file',
          inputSchema: {
            type: 'object',
            properties: {
              path: {
                type: 'string',
                description: 'Path to the file to delete'
              }
            },
            required: ['path']
          }
        },
        {
          name: 'file_copy',
          description: 'Copy a file to another location',
          inputSchema: {
            type: 'object',
            properties: {
              source: {
                type: 'string',
                description: 'Source file path'
              },
              destination: {
                type: 'string',
                description: 'Destination file path'
              }
            },
            required: ['source', 'destination']
          }
        },
        {
          name: 'file_move',
          description: 'Move/rename a file',
          inputSchema: {
            type: 'object',
            properties: {
              source: {
                type: 'string',
                description: 'Source file path'
              },
              destination: {
                type: 'string',
                description: 'Destination file path'
              }
            },
            required: ['source', 'destination']
          }
        }
      ]
    };
  }

  async callTool(name: string, args: any) {
    switch (name) {
      case 'file_read':
        return await this.readFile(args.path);
      case 'file_write':
        return await this.writeFile(args.path, args.content, args.createDir);
      case 'file_delete':
        return await this.deleteFile(args.path);
      case 'file_copy':
        return await this.copyFile(args.source, args.destination);
      case 'file_move':
        return await this.moveFile(args.source, args.destination);
      default:
        throw new Error(`Unknown file tool: ${name}`);
    }
  }

  private async readFile(filePath: string) {
    const validatedPath = validatePath(filePath, this.basePath);
    
    try {
      const content = await fs.readFile(validatedPath, 'utf-8');
      const stats = await fs.stat(validatedPath);
      
      return {
        content: [
          {
            type: 'text',
            text: content
          },
          {
            type: 'text',
            text: `\n\nFile info:\n- Size: ${stats.size} bytes\n- Modified: ${stats.mtime.toISOString()}\n- Type: ${stats.isFile() ? 'file' : 'directory'}`
          }
        ]
      };
    } catch (error) {
      throw new Error(`Failed to read file: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  private async writeFile(filePath: string, content: string, createDir: boolean = false) {
    const validatedPath = validatePath(filePath, this.basePath);
    
    try {
      // Create parent directories if requested
      if (createDir) {
        const dir = path.dirname(validatedPath);
        await fs.mkdir(dir, { recursive: true });
      }
      
      await fs.writeFile(validatedPath, content, 'utf-8');
      
      return {
        content: [
          {
            type: 'text',
            text: `Successfully wrote ${content.length} characters to ${filePath}`
          }
        ]
      };
    } catch (error) {
      throw new Error(`Failed to write file: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  private async deleteFile(filePath: string) {
    const validatedPath = validatePath(filePath, this.basePath);
    
    try {
      await fs.unlink(validatedPath);
      
      return {
        content: [
          {
            type: 'text',
            text: `Successfully deleted file: ${filePath}`
          }
        ]
      };
    } catch (error) {
      throw new Error(`Failed to delete file: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  private async copyFile(source: string, destination: string) {
    const validatedSource = validatePath(source, this.basePath);
    const validatedDest = validatePath(destination, this.basePath);
    
    try {
      await fs.copyFile(validatedSource, validatedDest);
      
      return {
        content: [
          {
            type: 'text',
            text: `Successfully copied ${source} to ${destination}`
          }
        ]
      };
    } catch (error) {
      throw new Error(`Failed to copy file: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  private async moveFile(source: string, destination: string) {
    const validatedSource = validatePath(source, this.basePath);
    const validatedDest = validatePath(destination, this.basePath);
    
    try {
      await fs.rename(validatedSource, validatedDest);
      
      return {
        content: [
          {
            type: 'text',
            text: `Successfully moved ${source} to ${destination}`
          }
        ]
      };
    } catch (error) {
      throw new Error(`Failed to move file: ${error instanceof Error ? error.message : String(error)}`);
    }
  }
}

Directory Tools

typescript
// src/tools/directory.ts
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import * as fs from 'fs/promises';
import * as path from 'path';
import { validatePath } from '../utils/validation.js';

export class DirectoryTools {
  private basePath: string;

  constructor(basePath: string = process.cwd()) {
    this.basePath = path.resolve(basePath);
  }

  async listTools(): Promise<{ tools: Tool[] }> {
    return {
      tools: [
        {
          name: 'directory_list',
          description: 'List contents of a directory',
          inputSchema: {
            type: 'object',
            properties: {
              path: {
                type: 'string',
                description: 'Path to the directory to list'
              },
              recursive: {
                type: 'boolean',
                description: 'List files recursively',
                default: false
              },
              showHidden: {
                type: 'boolean',
                description: 'Show hidden files (starting with .)',
                default: false
              }
            },
            required: ['path']
          }
        },
        {
          name: 'directory_create',
          description: 'Create a new directory',
          inputSchema: {
            type: 'object',
            properties: {
              path: {
                type: 'string',
                description: 'Path to the directory to create'
              },
              recursive: {
                type: 'boolean',
                description: 'Create parent directories if they don\'t exist',
                default: true
              }
            },
            required: ['path']
          }
        },
        {
          name: 'directory_delete',
          description: 'Delete a directory',
          inputSchema: {
            type: 'object',
            properties: {
              path: {
                type: 'string',
                description: 'Path to the directory to delete'
              },
              recursive: {
                type: 'boolean',
                description: 'Delete directory and all its contents',
                default: false
              }
            },
            required: ['path']
          }
        },
        {
          name: 'directory_stats',
          description: 'Get statistics about a directory',
          inputSchema: {
            type: 'object',
            properties: {
              path: {
                type: 'string',
                description: 'Path to the directory'
              }
            },
            required: ['path']
          }
        }
      ]
    };
  }

  async callTool(name: string, args: any) {
    switch (name) {
      case 'directory_list':
        return await this.listDirectory(args.path, args.recursive, args.showHidden);
      case 'directory_create':
        return await this.createDirectory(args.path, args.recursive);
      case 'directory_delete':
        return await this.deleteDirectory(args.path, args.recursive);
      case 'directory_stats':
        return await this.getDirectoryStats(args.path);
      default:
        throw new Error(`Unknown directory tool: ${name}`);
    }
  }

  private async listDirectory(dirPath: string, recursive: boolean = false, showHidden: boolean = false) {
    const validatedPath = validatePath(dirPath, this.basePath);
    
    try {
      const items = await this.readDirectory(validatedPath, recursive, showHidden);
      
      return {
        content: [
          {
            type: 'text',
            text: `Directory listing for ${dirPath}:\n\n${items.join('\n')}`
          }
        ]
      };
    } catch (error) {
      throw new Error(`Failed to list directory: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  private async readDirectory(dirPath: string, recursive: boolean, showHidden: boolean): Promise<string[]> {
    const items: string[] = [];
    const entries = await fs.readdir(dirPath, { withFileTypes: true });
    
    for (const entry of entries) {
      if (!showHidden && entry.name.startsWith('.')) {
        continue;
      }
      
      const fullPath = path.join(dirPath, entry.name);
      const relativePath = path.relative(this.basePath, fullPath);
      
      items.push(`${entry.isDirectory() ? '📁' : '📄'} ${relativePath}`);
      
      if (recursive && entry.isDirectory()) {
        const subItems = await this.readDirectory(fullPath, recursive, showHidden);
        items.push(...subItems.map(item => '  ' + item));
      }
    }
    
    return items;
  }

  private async createDirectory(dirPath: string, recursive: boolean = true) {
    const validatedPath = validatePath(dirPath, this.basePath);
    
    try {
      await fs.mkdir(validatedPath, { recursive });
      
      return {
        content: [
          {
            type: 'text',
            text: `Successfully created directory: ${dirPath}`
          }
        ]
      };
    } catch (error) {
      throw new Error(`Failed to create directory: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  private async deleteDirectory(dirPath: string, recursive: boolean = false) {
    const validatedPath = validatePath(dirPath, this.basePath);
    
    try {
      if (recursive) {
        await fs.rm(validatedPath, { recursive: true, force: true });
      } else {
        await fs.rmdir(validatedPath);
      }
      
      return {
        content: [
          {
            type: 'text',
            text: `Successfully deleted directory: ${dirPath}`
          }
        ]
      };
    } catch (error) {
      throw new Error(`Failed to delete directory: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  private async getDirectoryStats(dirPath: string) {
    const validatedPath = validatePath(dirPath, this.basePath);
    
    try {
      const stats = await this.calculateDirectoryStats(validatedPath);
      
      return {
        content: [
          {
            type: 'text',
            text: `Directory statistics for ${dirPath}:\n\n` +
                  `- Total files: ${stats.fileCount}\n` +
                  `- Total directories: ${stats.dirCount}\n` +
                  `- Total size: ${stats.totalSize} bytes\n` +
                  `- Largest file: ${stats.largestFile}\n` +
                  `- Average file size: ${stats.avgFileSize} bytes`
          }
        ]
      };
    } catch (error) {
      throw new Error(`Failed to get directory stats: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  private async calculateDirectoryStats(dirPath: string): Promise<{
    fileCount: number;
    dirCount: number;
    totalSize: number;
    largestFile: string;
    avgFileSize: number;
  }> {
    let fileCount = 0;
    let dirCount = 0;
    let totalSize = 0;
    let largestFile = '';
    let maxFileSize = 0;
    
    const entries = await fs.readdir(dirPath, { withFileTypes: true });
    
    for (const entry of entries) {
      const fullPath = path.join(dirPath, entry.name);
      
      if (entry.isDirectory()) {
        dirCount++;
        const subStats = await this.calculateDirectoryStats(fullPath);
        fileCount += subStats.fileCount;
        dirCount += subStats.dirCount;
        totalSize += subStats.totalSize;
        if (subStats.totalSize > maxFileSize) {
          maxFileSize = subStats.totalSize;
          largestFile = subStats.largestFile;
        }
      } else {
        fileCount++;
        const stats = await fs.stat(fullPath);
        totalSize += stats.size;
        if (stats.size > maxFileSize) {
          maxFileSize = stats.size;
          largestFile = path.relative(this.basePath, fullPath);
        }
      }
    }
    
    return {
      fileCount,
      dirCount,
      totalSize,
      largestFile,
      avgFileSize: fileCount > 0 ? Math.round(totalSize / fileCount) : 0
    };
  }
}

Step 3: Implement Resources

File Resources

typescript
// src/resources/file.ts
import { Resource, ReadResourceResult } from '@modelcontextprotocol/sdk/types.js';
import * as fs from 'fs/promises';
import * as path from 'path';
import { validatePath } from '../utils/validation.js';

export class FileResources {
  private basePath: string;

  constructor(basePath: string = process.cwd()) {
    this.basePath = path.resolve(basePath);
  }

  async listResources(): Promise<{ resources: Resource[] }> {
    const resources: Resource[] = [];
    
    // Add common configuration files as resources
    const configFiles = [
      'package.json',
      'tsconfig.json',
      '.gitignore',
      'README.md',
      '.env.example'
    ];
    
    for (const file of configFiles) {
      const filePath = path.join(this.basePath, file);
      try {
        await fs.access(filePath);
        resources.push({
          uri: `file://${file}`,
          name: file,
          description: `Configuration file: ${file}`,
          mimeType: this.getMimeType(file)
        });
      } catch {
        // File doesn't exist, skip
      }
    }
    
    return { resources };
  }

  async readResource(uri: string): Promise<ReadResourceResult> {
    if (!uri.startsWith('file://')) {
      throw new Error(`Unsupported URI scheme: ${uri}`);
    }
    
    const filePath = uri.replace('file://', '');
    const validatedPath = validatePath(filePath, this.basePath);
    
    try {
      const content = await fs.readFile(validatedPath, 'utf-8');
      const stats = await fs.stat(validatedPath);
      
      return {
        contents: [
          {
            uri,
            mimeType: this.getMimeType(filePath),
            text: content
          }
        ]
      };
    } catch (error) {
      throw new Error(`Failed to read resource: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  private getMimeType(filePath: string): string {
    const ext = path.extname(filePath).toLowerCase();
    const mimeTypes: { [key: string]: string } = {
      '.json': 'application/json',
      '.js': 'text/javascript',
      '.ts': 'text/typescript',
      '.md': 'text/markdown',
      '.txt': 'text/plain',
      '.yml': 'text/yaml',
      '.yaml': 'text/yaml',
      '.xml': 'application/xml',
      '.html': 'text/html',
      '.css': 'text/css'
    };
    
    return mimeTypes[ext] || 'text/plain';
  }
}

Step 4: Validation Utilities

typescript
// src/utils/validation.ts
import * as path from 'path';

export function validatePath(inputPath: string, basePath: string): string {
  // Resolve the input path
  const resolvedPath = path.resolve(basePath, inputPath);
  
  // Check if the resolved path is within the base path
  if (!resolvedPath.startsWith(basePath)) {
    throw new Error(`Path traversal detected: ${inputPath}`);
  }
  
  return resolvedPath;
}

export function validateFileName(fileName: string): void {
  // Check for invalid characters
  const invalidChars = /[<>:"|?*]/;
  if (invalidChars.test(fileName)) {
    throw new Error(`Invalid file name: ${fileName}`);
  }
  
  // Check for reserved names (Windows)
  const reservedNames = [
    'CON', 'PRN', 'AUX', 'NUL',
    'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
    'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'
  ];
  
  const nameWithoutExt = path.parse(fileName).name.toUpperCase();
  if (reservedNames.includes(nameWithoutExt)) {
    throw new Error(`Reserved file name: ${fileName}`);
  }
}

Step 5: Build và Run

Update Package.json

json
{
  "name": "filesystem-mcp-server",
  "version": "1.0.0",
  "description": "File system MCP server for AI agents",
  "main": "dist/index.js",
  "bin": {
    "filesystem-mcp-server": "dist/index.js"
  },
  "scripts": {
    "build": "tsc",
    "dev": "nodemon --exec ts-node src/index.ts",
    "start": "node dist/index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": ["mcp", "filesystem", "ai"],
  "author": "Your Name",
  "license": "MIT",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "nodemon": "^3.0.0",
    "ts-node": "^10.9.0",
    "typescript": "^5.0.0"
  }
}

Build Project

bash
# Build TypeScript
npm run build

# Run server
npm start

Test với Claude Desktop

json
// claude_desktop_config.json
{
  "mcpServers": {
    "filesystem": {
      "command": "node",
      "args": ["/path/to/my-mcp-server/dist/index.js"],
      "env": {
        "BASE_PATH": "/path/to/allowed/directory"
      }
    }
  }
}

Step 6: Advanced Features

Add File Watching

typescript
// src/tools/watch.ts
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import * as fs from 'fs';
import * as path from 'path';

export class FileWatcher {
  private watchers: Map<string, fs.FSWatcher> = new Map();

  async watchFile(filePath: string, callback: (event: string, filename: string) => void) {
    const watcher = fs.watch(filePath, callback);
    this.watchers.set(filePath, watcher);
  }

  async unwatchFile(filePath: string) {
    const watcher = this.watchers.get(filePath);
    if (watcher) {
      watcher.close();
      this.watchers.delete(filePath);
    }
  }

  async listTools(): Promise<{ tools: Tool[] }> {
    return {
      tools: [
        {
          name: 'file_watch',
          description: 'Watch a file for changes',
          inputSchema: {
            type: 'object',
            properties: {
              path: { type: 'string' },
              events: {
                type: 'array',
                items: { type: 'string', enum: ['change', 'rename'] }
              }
            },
            required: ['path']
          }
        }
      ]
    };
  }
}

Add Search Functionality

typescript
// src/tools/search.ts
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import * as fs from 'fs/promises';
import * as path from 'path';

export class FileSearch {
  async searchFiles(dirPath: string, pattern: string, recursive: boolean = true): Promise<string[]> {
    const results: string[] = [];
    const regex = new RegExp(pattern, 'i');
    
    async function search(currentPath: string) {
      const entries = await fs.readdir(currentPath, { withFileTypes: true });
      
      for (const entry of entries) {
        const fullPath = path.join(currentPath, entry.name);
        
        if (entry.isDirectory() && recursive) {
          await search(fullPath);
        } else if (entry.isFile() && regex.test(entry.name)) {
          results.push(fullPath);
        }
      }
    }
    
    await search(dirPath);
    return results;
  }
}

Testing

Unit Tests

typescript
// tests/file-tools.test.ts
import { FileTools } from '../src/tools/file.js';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as os from 'os';

describe('FileTools', () => {
  let fileTools: FileTools;
  let tempDir: string;

  beforeEach(async () => {
    tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-test-'));
    fileTools = new FileTools(tempDir);
  });

  afterEach(async () => {
    await fs.rm(tempDir, { recursive: true, force: true });
  });

  test('should read file', async () => {
    const testFile = path.join(tempDir, 'test.txt');
    await fs.writeFile(testFile, 'Hello, World!');
    
    const result = await fileTools.callTool('file_read', { path: 'test.txt' });
    
    expect(result.content[0].text).toContain('Hello, World!');
  });

  test('should write file', async () => {
    const result = await fileTools.callTool('file_write', {
      path: 'new-file.txt',
      content: 'Test content'
    });
    
    expect(result.content[0].text).toContain('Successfully wrote');
    
    const filePath = path.join(tempDir, 'new-file.txt');
    const content = await fs.readFile(filePath, 'utf-8');
    expect(content).toBe('Test content');
  });
});

Integration Tests

typescript
// tests/integration.test.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

describe('MCP Server Integration', () => {
  test('should handle tool calls', async () => {
    // Test complete MCP workflow
    // 1. Start server
    // 2. Send initialize request
    // 3. List tools
    // 4. Call tool
    // 5. Verify response
  });
});

Deployment

Dockerfile

dockerfile
FROM node:20-alpine

WORKDIR /app

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

COPY dist/ ./dist/

EXPOSE 3000

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

Docker Compose

yaml
version: '3.8'
services:
  filesystem-mcp-server:
    build: .
    volumes:
      - ./data:/data:ro
    environment:
      - BASE_PATH=/data
      - LOG_LEVEL=info

Summary

Built MCP Server với:

  • ✅ Complete tool implementation
  • ✅ Resource support
  • ✅ Input validation
  • ✅ Error handling
  • ✅ TypeScript support
  • ✅ Testing framework
  • ✅ Docker deployment

Key Features:

  • File operations: read, write, delete, copy, move
  • Directory operations: list, create, delete, stats
  • Resource access: configuration files
  • Security: path validation, sandboxing
  • Extensibility: easy to add new tools

Next Steps:

  • Add more tools (search, watch, etc.)
  • Implement authentication
  • Add monitoring/logging
  • Create custom prompts
  • Deploy to production

Tiếp theo: 6. MCP trong Agent IDE → Integration với development tools

Internal documentation for iNET Portal