MCP入门教程-第6章:MCP 根 (Roots)

57 阅读5分钟

什么是 MCP Roots?

Roots 是 MCP 中的一个概念,用于定义服务器可以操作的边界。它们为客户端提供了一种向服务器告知相关资源及其位置的方式。

简单来说,Root 是客户端建议服务器应该关注的 URI。当客户端连接到服务器时,它会声明服务器应该使用哪些根。虽然主要用于文件系统路径,但根可以是任何有效的 URI,包括 HTTP URL。

Roots 的典型示例

根可以是以下任何形式:

  • file:///home/user/project - 本地项目目录
  • https://api.example.com - API 端点
  • git://github.com/user/repo - Git 仓库
  • s3://bucket-name/path - 云存储路径

为什么使用 Roots?

Roots 在 MCP 架构中发挥着重要作用:

  1. 指导性:告知服务器相关资源和位置
  2. 清晰性:明确哪些资源是工作空间的一部分
  3. 组织性:允许同时处理不同的资源集合
  4. 边界定义:为服务器操作设定明确的范围

Roots 的工作机制

当客户端支持 Roots 时,它会:

  • 在连接期间声明 roots 能力
  • 向服务器提供建议的根列表
  • 在根发生变化时通知服务器(如果支持)

而服务器应该:

  • 尊重提供的根
  • 使用根 URI 来定位和访问资源
  • 优先在根边界内进行操作

TypeScript SDK 实现示例

下面我们通过 TypeScript 代码来演示如何在 MCP 中实现和使用 Roots。

1. 定义 Root 接口

interface MCPRoot {
  uri: string;
  name?: string;
  description?: string;
}

interface RootsCapability {
  listChanged?: boolean; // 是否支持根列表变更通知
}

interface ClientCapabilities {
  roots?: RootsCapability;
}

2. 客户端 Roots 管理器

class RootsManager {
  private roots: Map<string, MCPRoot> = new Map();
  private changeListeners: Array<(roots: MCPRoot[]) => void> = [];

  constructor(private client: MCPClient) {}

  /**
   * 添加一个新的根
   */
  addRoot(root: MCPRoot): void {
    this.roots.set(root.uri, root);
    this.notifyChange();
  }

  /**
   * 移除指定的根
   */
  removeRoot(uri: string): boolean {
    const removed = this.roots.delete(uri);
    if (removed) {
      this.notifyChange();
    }
    return removed;
  }

  /**
   * 获取所有根
   */
  getAllRoots(): MCPRoot[] {
    return Array.from(this.roots.values());
  }

  /**
   * 根据 URI 查找根
   */
  findRoot(uri: string): MCPRoot | undefined {
    return this.roots.get(uri);
  }

  /**
   * 检查 URI 是否在任何根的范围内
   */
  isWithinRoots(uri: string): boolean {
    return Array.from(this.roots.keys()).some(rootUri => 
      uri.startsWith(rootUri)
    );
  }

  /**
   * 添加变更监听器
   */
  onRootsChanged(listener: (roots: MCPRoot[]) => void): void {
    this.changeListeners.push(listener);
  }

  /**
   * 通知所有监听器根列表已变更
   */
  private notifyChange(): void {
    const currentRoots = this.getAllRoots();
    this.changeListeners.forEach(listener => listener(currentRoots));
  }
}

3. MCP 客户端实现

class MCPClient {
  private rootsManager: RootsManager;
  private serverConnection: ServerConnection;

  constructor() {
    this.rootsManager = new RootsManager(this);
    this.setupRootsChangeHandler();
  }

  /**
   * 初始化连接并发送根信息
   */
  async initialize(): Promise<void> {
    // 声明客户端能力
    const capabilities: ClientCapabilities = {
      roots: {
        listChanged: true // 支持根列表变更通知
      }
    };

    await this.serverConnection.initialize(capabilities);
    
    // 发送初始根列表
    await this.sendRootsToServer();
  }

  /**
   * 向服务器发送根列表
   */
  private async sendRootsToServer(): Promise<void> {
    const roots = this.rootsManager.getAllRoots();
    
    const message = {
      jsonrpc: "2.0",
      method: "notifications/roots/list_changed",
      params: {
        roots: roots.map(root => ({
          uri: root.uri,
          name: root.name
        }))
      }
    };

    await this.serverConnection.send(message);
  }

  /**
   * 设置根变更处理器
   */
  private setupRootsChangeHandler(): void {
    this.rootsManager.onRootsChanged(async (roots) => {
      if (this.serverConnection.isConnected()) {
        await this.sendRootsToServer();
      }
    });
  }

  /**
   * 获取根管理器
   */
  getRootsManager(): RootsManager {
    return this.rootsManager;
  }
}

4. 服务器端 Roots 处理

class MCPServer {
  private allowedRoots: Set<string> = new Set();

  /**
   * 处理根列表变更通知
   */
  async handleRootsListChanged(params: { roots: MCPRoot[] }): Promise<void> {
    this.allowedRoots.clear();
    
    params.roots.forEach(root => {
      this.allowedRoots.add(root.uri);
      console.log(`Added root: ${root.uri} (${root.name || 'unnamed'})`);
    });

    // 验证当前操作是否在允许的根范围内
    await this.validateCurrentOperations();
  }

  /**
   * 检查资源是否在允许的根范围内
   */
  isResourceAllowed(resourceUri: string): boolean {
    if (this.allowedRoots.size === 0) {
      return true; // 如果没有设置根,则允许所有资源
    }

    return Array.from(this.allowedRoots).some(rootUri => 
      resourceUri.startsWith(rootUri)
    );
  }

  /**
   * 安全的资源访问方法
   */
  async accessResource(resourceUri: string): Promise<any> {
    if (!this.isResourceAllowed(resourceUri)) {
      throw new Error(`Access denied: ${resourceUri} is outside allowed roots`);
    }

    // 执行实际的资源访问
    return await this.performResourceAccess(resourceUri);
  }

  private async performResourceAccess(resourceUri: string): Promise<any> {
    // 实际的资源访问逻辑
    console.log(`Accessing resource: ${resourceUri}`);
  }

  private async validateCurrentOperations(): Promise<void> {
    // 验证当前正在进行的操作是否仍然有效
    console.log('Validating current operations against new roots...');
  }
}

5. 完整使用示例

async function demonstrateMCPRoots() {
  // 创建 MCP 客户端
  const client = new MCPClient();
  const rootsManager = client.getRootsManager();

  // 添加项目根目录
  rootsManager.addRoot({
    uri: "file:///home/user/my-project",
    name: "Main Project",
    description: "主项目目录"
  });

  // 添加 API 端点根
  rootsManager.addRoot({
    uri: "https://api.myservice.com/v1",
    name: "API Endpoint",
    description: "主要 API 服务端点"
  });

  // 添加配置目录根
  rootsManager.addRoot({
    uri: "file:///etc/myapp",
    name: "Configuration",
    description: "应用配置目录"
  });

  // 初始化连接
  await client.initialize();

  // 检查资源是否在根范围内
  const testUris = [
    "file:///home/user/my-project/src/main.ts",
    "https://api.myservice.com/v1/users",
    "file:///tmp/temp-file.txt"
  ];

  testUris.forEach(uri => {
    const isWithin = rootsManager.isWithinRoots(uri);
    console.log(`${uri}: ${isWithin ? '✓ 在根范围内' : '✗ 不在根范围内'}`);
  });

  // 动态更新根
  setTimeout(() => {
    rootsManager.addRoot({
      uri: "file:///home/user/cache",
      name: "Cache Directory",
      description: "缓存目录"
    });
    console.log('添加了新的根目录');
  }, 5000);
}

// 运行演示
demonstrateMCPRoots().catch(console.error);

最佳实践

在使用 MCP Roots 时,应遵循以下最佳实践:

1. 精确性原则

只建议必要的资源,避免过度暴露不相关的目录或端点。

// ✅ 好的做法:精确的根定义
rootsManager.addRoot({
  uri: "file:///home/user/project/src",
  name: "Source Code"
});

// ❌ 避免:过于宽泛的根定义
rootsManager.addRoot({
  uri: "file:///",
  name: "Entire Filesystem"
});

2. 清晰的命名

为根使用清晰、描述性的名称。

rootsManager.addRoot({
  uri: "https://api.example.com/v2",
  name: "User Management API",
  description: "用户管理相关的 API 端点"
});

3. 优雅的错误处理

妥善处理根变更和访问错误。

class SafeRootsManager extends RootsManager {
  async safeAddRoot(root: MCPRoot): Promise<boolean> {
    try {
      // 验证根的可访问性
      await this.validateRootAccess(root.uri);
      this.addRoot(root);
      return true;
    } catch (error) {
      console.error(`Failed to add root ${root.uri}:`, error);
      return false;
    }
  }

  private async validateRootAccess(uri: string): Promise<void> {
    // 根据 URI 类型进行相应的验证
    if (uri.startsWith('file://')) {
      // 验证文件系统访问
    } else if (uri.startsWith('http')) {
      // 验证 HTTP 端点访问
    }
  }
}

常见用例

1. 多项目工作空间

const workspaceRoots = [
  { uri: "file:///home/user/frontend", name: "Frontend Project" },
  { uri: "file:///home/user/backend", name: "Backend Project" },
  { uri: "file:///home/user/shared", name: "Shared Libraries" }
];

workspaceRoots.forEach(root => rootsManager.addRoot(root));

2. 混合资源访问

const mixedRoots = [
  { uri: "file:///project/local", name: "Local Files" },
  { uri: "https://api.service.com", name: "Remote API" },
  { uri: "s3://my-bucket/data", name: "Cloud Storage" }
];

mixedRoots.forEach(root => rootsManager.addRoot(root));

总结

MCP Roots 是一个强大的概念,它为 AI 应用程序提供了清晰的资源边界定义机制。通过合理使用 Roots,我们可以:

  • 提高 AI 系统的安全性和可控性
  • 优化资源访问的效率
  • 简化复杂工作环境的管理
  • 实现更好的权限控制

在实际项目中,建议根据具体需求灵活使用 Roots 概念,并始终遵循最佳实践来确保系统的稳定性和安全性。随着 MCP 生态系统的不断发展,Roots 概念将在 AI 应用程序的集成中发挥越来越重要的作用。