feat: 实现 MCP 工具集中式路径验证,增强安全性和可配置性

- 新增 path-validator.ts:参考 MCP filesystem 服务器设计的集中式路径验证器
  - 支持 CCW_PROJECT_ROOT 和 CCW_ALLOWED_DIRS 环境变量配置
  - 多层路径验证:绝对路径解析 → 沙箱检查 → 符号链接验证
  - 向后兼容:未设置环境变量时回退到 process.cwd()

- 更新所有 MCP 工具使用集中式路径验证:
  - write-file.ts: 使用 validatePath()
  - edit-file.ts: 使用 validatePath({ mustExist: true })
  - read-file.ts: 使用 validatePath() + getProjectRoot()
  - smart-search.ts: 使用 getProjectRoot()
  - core-memory.ts: 使用 getProjectRoot()

- MCP 服务器启动时输出项目根目录和允许目录信息

- MCP 管理界面增强:
  - CCW Tools 安装卡片新增路径设置 UI
  - 支持 CCW_PROJECT_ROOT 和 CCW_ALLOWED_DIRS 配置
  - 添加"使用当前项目"快捷按钮
  - 支持 Claude 和 Codex 两种模式
  - 添加中英文国际化翻译

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
catlog22
2025-12-21 18:14:06 +08:00
parent f492f4839a
commit 45f92fe066
11 changed files with 330 additions and 16 deletions

View File

@@ -40,6 +40,22 @@ function getCcwEnabledToolsCodex() {
return CCW_MCP_TOOLS.filter(t => t.core).map(t => t.name);
}
// Get current CCW_PROJECT_ROOT from config
function getCcwProjectRoot() {
const currentPath = projectPath;
const projectData = mcpAllProjects[currentPath] || {};
const ccwConfig = projectData.mcpServers?.['ccw-tools'];
return ccwConfig?.env?.CCW_PROJECT_ROOT || '';
}
// Get current CCW_ALLOWED_DIRS from config
function getCcwAllowedDirs() {
const currentPath = projectPath;
const projectData = mcpAllProjects[currentPath] || {};
const ccwConfig = projectData.mcpServers?.['ccw-tools'];
return ccwConfig?.env?.CCW_ALLOWED_DIRS || '';
}
async function renderMcpManager() {
const container = document.getElementById('mainContent');
if (!container) return;
@@ -232,6 +248,34 @@ async function renderMcpManager() {
<button class="text-primary hover:underline" onclick="selectCcwToolsCodex('all')">All</button>
<button class="text-muted-foreground hover:underline" onclick="selectCcwToolsCodex('none')">None</button>
</div>
<!-- Path Settings -->
<div class="ccw-path-settings mt-3 pt-3 border-t border-border/50">
<div class="flex items-center gap-2 mb-2">
<i data-lucide="folder-root" class="w-4 h-4 text-muted-foreground"></i>
<span class="text-xs font-medium text-muted-foreground">${t('mcp.pathSettings')}</span>
</div>
<div class="grid grid-cols-1 gap-2">
<div class="flex items-center gap-2">
<label class="text-xs text-muted-foreground w-28 shrink-0">CCW_PROJECT_ROOT</label>
<input type="text"
class="ccw-project-root-input flex-1 px-2 py-1 text-xs bg-background border border-border rounded focus:outline-none focus:ring-1 focus:ring-primary"
placeholder="${projectPath || t('mcp.useCurrentDir')}"
value="${getCcwProjectRoot()}">
<button class="p-1 text-muted-foreground hover:text-foreground"
onclick="setCcwProjectRootToCurrent()"
title="${t('mcp.useCurrentProject')}">
<i data-lucide="locate-fixed" class="w-4 h-4"></i>
</button>
</div>
<div class="flex items-center gap-2">
<label class="text-xs text-muted-foreground w-28 shrink-0">CCW_ALLOWED_DIRS</label>
<input type="text"
class="ccw-allowed-dirs-input flex-1 px-2 py-1 text-xs bg-background border border-border rounded focus:outline-none focus:ring-1 focus:ring-primary"
placeholder="${t('mcp.allowedDirsPlaceholder')}"
value="${getCcwAllowedDirs()}">
</div>
</div>
</div>
</div>
</div>
<div class="shrink-0">
@@ -418,6 +462,34 @@ async function renderMcpManager() {
<button class="text-primary hover:underline" onclick="selectCcwTools('all')">All</button>
<button class="text-muted-foreground hover:underline" onclick="selectCcwTools('none')">None</button>
</div>
<!-- Path Settings -->
<div class="ccw-path-settings mt-3 pt-3 border-t border-border/50">
<div class="flex items-center gap-2 mb-2">
<i data-lucide="folder-root" class="w-4 h-4 text-muted-foreground"></i>
<span class="text-xs font-medium text-muted-foreground">${t('mcp.pathSettings')}</span>
</div>
<div class="grid grid-cols-1 gap-2">
<div class="flex items-center gap-2">
<label class="text-xs text-muted-foreground w-28 shrink-0">CCW_PROJECT_ROOT</label>
<input type="text"
class="ccw-project-root-input flex-1 px-2 py-1 text-xs bg-background border border-border rounded focus:outline-none focus:ring-1 focus:ring-primary"
placeholder="${projectPath || t('mcp.useCurrentDir')}"
value="${getCcwProjectRoot()}">
<button class="p-1 text-muted-foreground hover:text-foreground"
onclick="setCcwProjectRootToCurrent()"
title="${t('mcp.useCurrentProject')}">
<i data-lucide="locate-fixed" class="w-4 h-4"></i>
</button>
</div>
<div class="flex items-center gap-2">
<label class="text-xs text-muted-foreground w-28 shrink-0">CCW_ALLOWED_DIRS</label>
<input type="text"
class="ccw-allowed-dirs-input flex-1 px-2 py-1 text-xs bg-background border border-border rounded focus:outline-none focus:ring-1 focus:ring-primary"
placeholder="${t('mcp.allowedDirsPlaceholder')}"
value="${getCcwAllowedDirs()}">
</div>
</div>
</div>
</div>
</div>
<div class="shrink-0 flex gap-2">